aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Desprez <jdesprez@google.com>2018-10-22 11:37:22 -0700
committerandroid-build-merger <android-build-merger@google.com>2018-10-22 11:37:22 -0700
commit13217871fefa43f6d16fbb31b04e9904996d87d5 (patch)
treeede84fcf0a9687d4907ae5f8a4788271d62e0922
parentcfbefd32336596ea63784607e4106dc37ce0567f (diff)
parent6fbc3cf5a1a3369fd354c1e5d9f90c86e4bce0a4 (diff)
downloadopencensus-java-13217871fefa43f6d16fbb31b04e9904996d87d5.tar.gz
Merge remote-tracking branch 'aosp/upstream-master' into merge am: dd3cabeacc
am: 6fbc3cf5a1 Change-Id: I11b0ec1cf561d2a14da78e444b1594f167787fe6
-rw-r--r--.github/ISSUE_TEMPLATE16
-rw-r--r--.gitignore46
-rw-r--r--.gitmodules0
-rw-r--r--.travis.yml90
-rw-r--r--AUTHORS1
-rw-r--r--CHANGELOG.md150
-rw-r--r--CONTRIBUTING.md158
-rw-r--r--LICENSE202
-rw-r--r--README.md306
-rw-r--r--RELEASING.md294
-rw-r--r--all/build.gradle105
-rw-r--r--api/README.md6
-rw-r--r--api/build.gradle15
-rw-r--r--api/src/main/java/io/opencensus/common/Clock.java43
-rw-r--r--api/src/main/java/io/opencensus/common/Duration.java136
-rw-r--r--api/src/main/java/io/opencensus/common/ExperimentalApi.java58
-rw-r--r--api/src/main/java/io/opencensus/common/Function.java38
-rw-r--r--api/src/main/java/io/opencensus/common/Functions.java130
-rw-r--r--api/src/main/java/io/opencensus/common/Internal.java42
-rw-r--r--api/src/main/java/io/opencensus/common/NonThrowingCloseable.java42
-rw-r--r--api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java35
-rw-r--r--api/src/main/java/io/opencensus/common/Scope.java37
-rw-r--r--api/src/main/java/io/opencensus/common/ServerStats.java86
-rw-r--r--api/src/main/java/io/opencensus/common/ServerStatsDeserializationException.java47
-rw-r--r--api/src/main/java/io/opencensus/common/ServerStatsEncoding.java125
-rw-r--r--api/src/main/java/io/opencensus/common/ServerStatsFieldEnums.java159
-rw-r--r--api/src/main/java/io/opencensus/common/TimeUtils.java59
-rw-r--r--api/src/main/java/io/opencensus/common/Timestamp.java200
-rw-r--r--api/src/main/java/io/opencensus/common/ToDoubleFunction.java41
-rw-r--r--api/src/main/java/io/opencensus/common/ToLongFunction.java40
-rw-r--r--api/src/main/java/io/opencensus/common/package-info.java18
-rw-r--r--api/src/main/java/io/opencensus/internal/DefaultVisibilityForTesting.java37
-rw-r--r--api/src/main/java/io/opencensus/internal/NoopScope.java38
-rw-r--r--api/src/main/java/io/opencensus/internal/Provider.java49
-rw-r--r--api/src/main/java/io/opencensus/internal/StringUtils.java42
-rw-r--r--api/src/main/java/io/opencensus/internal/Utils.java191
-rw-r--r--api/src/main/java/io/opencensus/internal/ZeroTimeClock.java49
-rw-r--r--api/src/main/java/io/opencensus/internal/package-info.java24
-rw-r--r--api/src/main/java/io/opencensus/metrics/DerivedDoubleGauge.java152
-rw-r--r--api/src/main/java/io/opencensus/metrics/DerivedLongGauge.java150
-rw-r--r--api/src/main/java/io/opencensus/metrics/DoubleGauge.java213
-rw-r--r--api/src/main/java/io/opencensus/metrics/LabelKey.java62
-rw-r--r--api/src/main/java/io/opencensus/metrics/LabelValue.java57
-rw-r--r--api/src/main/java/io/opencensus/metrics/LongGauge.java205
-rw-r--r--api/src/main/java/io/opencensus/metrics/MetricRegistry.java156
-rw-r--r--api/src/main/java/io/opencensus/metrics/Metrics.java96
-rw-r--r--api/src/main/java/io/opencensus/metrics/MetricsComponent.java71
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/Distribution.java345
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/ExportComponent.java60
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/Metric.java137
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/MetricDescriptor.java173
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/MetricProducer.java40
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/MetricProducerManager.java88
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/Point.java63
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/Summary.java187
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/TimeSeries.java127
-rw-r--r--api/src/main/java/io/opencensus/metrics/export/Value.java246
-rw-r--r--api/src/main/java/io/opencensus/metrics/package-info.java32
-rw-r--r--api/src/main/java/io/opencensus/stats/Aggregation.java239
-rw-r--r--api/src/main/java/io/opencensus/stats/AggregationData.java555
-rw-r--r--api/src/main/java/io/opencensus/stats/BucketBoundaries.java66
-rw-r--r--api/src/main/java/io/opencensus/stats/Measure.java177
-rw-r--r--api/src/main/java/io/opencensus/stats/MeasureMap.java92
-rw-r--r--api/src/main/java/io/opencensus/stats/Measurement.java130
-rw-r--r--api/src/main/java/io/opencensus/stats/NoopStats.java221
-rw-r--r--api/src/main/java/io/opencensus/stats/Stats.java126
-rw-r--r--api/src/main/java/io/opencensus/stats/StatsCollectionState.java44
-rw-r--r--api/src/main/java/io/opencensus/stats/StatsComponent.java74
-rw-r--r--api/src/main/java/io/opencensus/stats/StatsRecorder.java34
-rw-r--r--api/src/main/java/io/opencensus/stats/View.java306
-rw-r--r--api/src/main/java/io/opencensus/stats/ViewData.java461
-rw-r--r--api/src/main/java/io/opencensus/stats/ViewManager.java61
-rw-r--r--api/src/main/java/io/opencensus/stats/package-info.java20
-rw-r--r--api/src/main/java/io/opencensus/tags/InternalUtils.java38
-rw-r--r--api/src/main/java/io/opencensus/tags/NoopTags.java214
-rw-r--r--api/src/main/java/io/opencensus/tags/Tag.java60
-rw-r--r--api/src/main/java/io/opencensus/tags/TagContext.java109
-rw-r--r--api/src/main/java/io/opencensus/tags/TagContextBuilder.java65
-rw-r--r--api/src/main/java/io/opencensus/tags/TagKey.java84
-rw-r--r--api/src/main/java/io/opencensus/tags/TagValue.java79
-rw-r--r--api/src/main/java/io/opencensus/tags/Tagger.java86
-rw-r--r--api/src/main/java/io/opencensus/tags/TaggingState.java48
-rw-r--r--api/src/main/java/io/opencensus/tags/Tags.java126
-rw-r--r--api/src/main/java/io/opencensus/tags/TagsComponent.java73
-rw-r--r--api/src/main/java/io/opencensus/tags/package-info.java32
-rw-r--r--api/src/main/java/io/opencensus/tags/propagation/TagContextBinarySerializer.java57
-rw-r--r--api/src/main/java/io/opencensus/tags/propagation/TagContextDeserializationException.java49
-rw-r--r--api/src/main/java/io/opencensus/tags/propagation/TagContextSerializationException.java49
-rw-r--r--api/src/main/java/io/opencensus/tags/propagation/TagPropagationComponent.java36
-rw-r--r--api/src/main/java/io/opencensus/tags/unsafe/ContextUtils.java56
-rw-r--r--api/src/main/java/io/opencensus/trace/Annotation.java83
-rw-r--r--api/src/main/java/io/opencensus/trace/AttributeValue.java255
-rw-r--r--api/src/main/java/io/opencensus/trace/BaseMessageEvent.java37
-rw-r--r--api/src/main/java/io/opencensus/trace/BlankSpan.java102
-rw-r--r--api/src/main/java/io/opencensus/trace/CurrentSpanUtils.java180
-rw-r--r--api/src/main/java/io/opencensus/trace/EndSpanOptions.java127
-rw-r--r--api/src/main/java/io/opencensus/trace/Link.java124
-rw-r--r--api/src/main/java/io/opencensus/trace/LowerCaseBase16Encoding.java91
-rw-r--r--api/src/main/java/io/opencensus/trace/MessageEvent.java151
-rw-r--r--api/src/main/java/io/opencensus/trace/NetworkEvent.java200
-rw-r--r--api/src/main/java/io/opencensus/trace/Sampler.java61
-rw-r--r--api/src/main/java/io/opencensus/trace/Span.java288
-rw-r--r--api/src/main/java/io/opencensus/trace/SpanBuilder.java356
-rw-r--r--api/src/main/java/io/opencensus/trace/SpanContext.java165
-rw-r--r--api/src/main/java/io/opencensus/trace/SpanId.java214
-rw-r--r--api/src/main/java/io/opencensus/trace/Status.java469
-rw-r--r--api/src/main/java/io/opencensus/trace/TraceComponent.java118
-rw-r--r--api/src/main/java/io/opencensus/trace/TraceId.java236
-rw-r--r--api/src/main/java/io/opencensus/trace/TraceOptions.java280
-rw-r--r--api/src/main/java/io/opencensus/trace/Tracer.java370
-rw-r--r--api/src/main/java/io/opencensus/trace/Tracestate.java273
-rw-r--r--api/src/main/java/io/opencensus/trace/Tracing.java125
-rw-r--r--api/src/main/java/io/opencensus/trace/config/TraceConfig.java64
-rw-r--r--api/src/main/java/io/opencensus/trace/config/TraceParams.java219
-rw-r--r--api/src/main/java/io/opencensus/trace/export/ExportComponent.java95
-rw-r--r--api/src/main/java/io/opencensus/trace/export/RunningSpanStore.java201
-rw-r--r--api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java525
-rw-r--r--api/src/main/java/io/opencensus/trace/export/SpanData.java477
-rw-r--r--api/src/main/java/io/opencensus/trace/export/SpanExporter.java96
-rw-r--r--api/src/main/java/io/opencensus/trace/internal/BaseMessageEventUtils.java81
-rw-r--r--api/src/main/java/io/opencensus/trace/package-info.java33
-rw-r--r--api/src/main/java/io/opencensus/trace/propagation/BinaryFormat.java156
-rw-r--r--api/src/main/java/io/opencensus/trace/propagation/PropagationComponent.java73
-rw-r--r--api/src/main/java/io/opencensus/trace/propagation/SpanContextParseException.java47
-rw-r--r--api/src/main/java/io/opencensus/trace/propagation/TextFormat.java204
-rw-r--r--api/src/main/java/io/opencensus/trace/samplers/AlwaysSampleSampler.java55
-rw-r--r--api/src/main/java/io/opencensus/trace/samplers/NeverSampleSampler.java55
-rw-r--r--api/src/main/java/io/opencensus/trace/samplers/ProbabilitySampler.java107
-rw-r--r--api/src/main/java/io/opencensus/trace/samplers/Samplers.java65
-rw-r--r--api/src/main/java/io/opencensus/trace/unsafe/ContextUtils.java45
-rw-r--r--api/src/test/java/io/opencensus/common/DurationTest.java152
-rw-r--r--api/src/test/java/io/opencensus/common/FunctionsTest.java64
-rw-r--r--api/src/test/java/io/opencensus/common/ServerStatsEncodingTest.java155
-rw-r--r--api/src/test/java/io/opencensus/common/ServerStatsFieldEnumsTest.java56
-rw-r--r--api/src/test/java/io/opencensus/common/ServerStatsTest.java78
-rw-r--r--api/src/test/java/io/opencensus/common/TimeUtilsTest.java60
-rw-r--r--api/src/test/java/io/opencensus/common/TimestampTest.java217
-rw-r--r--api/src/test/java/io/opencensus/internal/ProviderTest.java114
-rw-r--r--api/src/test/java/io/opencensus/internal/StringUtilsTest.java35
-rw-r--r--api/src/test/java/io/opencensus/internal/UtilsTest.java144
-rw-r--r--api/src/test/java/io/opencensus/metrics/DerivedDoubleGaugeTest.java89
-rw-r--r--api/src/test/java/io/opencensus/metrics/DerivedLongGaugeTest.java89
-rw-r--r--api/src/test/java/io/opencensus/metrics/DoubleGaugeTest.java88
-rw-r--r--api/src/test/java/io/opencensus/metrics/LabelKeyTest.java86
-rw-r--r--api/src/test/java/io/opencensus/metrics/LabelValueTest.java74
-rw-r--r--api/src/test/java/io/opencensus/metrics/LongGaugeTest.java87
-rw-r--r--api/src/test/java/io/opencensus/metrics/MetricRegistryTest.java220
-rw-r--r--api/src/test/java/io/opencensus/metrics/MetricsComponentTest.java40
-rw-r--r--api/src/test/java/io/opencensus/metrics/MetricsTest.java71
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/DistributionTest.java331
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/ExportComponentTest.java33
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/MetricDescriptorTest.java103
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/MetricProducerManagerTest.java80
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/MetricTest.java180
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/PointTest.java69
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/SummaryTest.java189
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/TimeSeriesTest.java151
-rw-r--r--api/src/test/java/io/opencensus/metrics/export/ValueTest.java168
-rw-r--r--api/src/test/java/io/opencensus/stats/AggregationDataTest.java231
-rw-r--r--api/src/test/java/io/opencensus/stats/AggregationTest.java97
-rw-r--r--api/src/test/java/io/opencensus/stats/BucketBoundariesTest.java87
-rw-r--r--api/src/test/java/io/opencensus/stats/MeasureTest.java129
-rw-r--r--api/src/test/java/io/opencensus/stats/NoopStatsTest.java133
-rw-r--r--api/src/test/java/io/opencensus/stats/NoopViewManagerTest.java186
-rw-r--r--api/src/test/java/io/opencensus/stats/StatsTest.java85
-rw-r--r--api/src/test/java/io/opencensus/stats/ViewDataTest.java301
-rw-r--r--api/src/test/java/io/opencensus/stats/ViewTest.java164
-rw-r--r--api/src/test/java/io/opencensus/tags/InternalUtilsTest.java44
-rw-r--r--api/src/test/java/io/opencensus/tags/NoopTagsTest.java179
-rw-r--r--api/src/test/java/io/opencensus/tags/TagContextTest.java103
-rw-r--r--api/src/test/java/io/opencensus/tags/TagKeyTest.java80
-rw-r--r--api/src/test/java/io/opencensus/tags/TagTest.java46
-rw-r--r--api/src/test/java/io/opencensus/tags/TagValueTest.java75
-rw-r--r--api/src/test/java/io/opencensus/tags/TagsTest.java85
-rw-r--r--api/src/test/java/io/opencensus/tags/propagation/TagContextDeserializationExceptionTest.java44
-rw-r--r--api/src/test/java/io/opencensus/tags/propagation/TagContextSerializationExceptionTest.java44
-rw-r--r--api/src/test/java/io/opencensus/tags/unsafe/ContextUtilsTest.java62
-rw-r--r--api/src/test/java/io/opencensus/trace/AnnotationTest.java102
-rw-r--r--api/src/test/java/io/opencensus/trace/AttributeValueTest.java233
-rw-r--r--api/src/test/java/io/opencensus/trace/BlankSpanTest.java68
-rw-r--r--api/src/test/java/io/opencensus/trace/CurrentSpanUtilsTest.java294
-rw-r--r--api/src/test/java/io/opencensus/trace/EndSpanOptionsTest.java80
-rw-r--r--api/src/test/java/io/opencensus/trace/LinkTest.java112
-rw-r--r--api/src/test/java/io/opencensus/trace/LowerCaseBase16EncodingTest.java83
-rw-r--r--api/src/test/java/io/opencensus/trace/MessageEventTest.java84
-rw-r--r--api/src/test/java/io/opencensus/trace/NetworkEventTest.java118
-rw-r--r--api/src/test/java/io/opencensus/trace/NoopSpan.java69
-rw-r--r--api/src/test/java/io/opencensus/trace/SpanBuilderTest.java104
-rw-r--r--api/src/test/java/io/opencensus/trace/SpanContextTest.java133
-rw-r--r--api/src/test/java/io/opencensus/trace/SpanIdTest.java90
-rw-r--r--api/src/test/java/io/opencensus/trace/SpanTest.java119
-rw-r--r--api/src/test/java/io/opencensus/trace/StatusTest.java54
-rw-r--r--api/src/test/java/io/opencensus/trace/TraceComponentTest.java59
-rw-r--r--api/src/test/java/io/opencensus/trace/TraceIdTest.java93
-rw-r--r--api/src/test/java/io/opencensus/trace/TraceOptionsTest.java98
-rw-r--r--api/src/test/java/io/opencensus/trace/TracerTest.java174
-rw-r--r--api/src/test/java/io/opencensus/trace/TracestateTest.java235
-rw-r--r--api/src/test/java/io/opencensus/trace/TracingTest.java83
-rw-r--r--api/src/test/java/io/opencensus/trace/config/TraceConfigTest.java51
-rw-r--r--api/src/test/java/io/opencensus/trace/config/TraceParamsTest.java97
-rw-r--r--api/src/test/java/io/opencensus/trace/export/ExportComponentTest.java46
-rw-r--r--api/src/test/java/io/opencensus/trace/export/NoopRunningSpanStoreTest.java55
-rw-r--r--api/src/test/java/io/opencensus/trace/export/NoopSampledSpanStoreTest.java94
-rw-r--r--api/src/test/java/io/opencensus/trace/export/SpanDataTest.java321
-rw-r--r--api/src/test/java/io/opencensus/trace/internal/BaseMessageEventUtilsTest.java71
-rw-r--r--api/src/test/java/io/opencensus/trace/propagation/BinaryFormatTest.java71
-rw-r--r--api/src/test/java/io/opencensus/trace/propagation/PropagationComponentTest.java36
-rw-r--r--api/src/test/java/io/opencensus/trace/propagation/SpanContextParseExceptionTest.java42
-rw-r--r--api/src/test/java/io/opencensus/trace/propagation/TextFormatTest.java75
-rw-r--r--api/src/test/java/io/opencensus/trace/samplers/SamplersTest.java281
-rw-r--r--appveyor.yml10
-rw-r--r--benchmarks/README.md3
-rw-r--r--benchmarks/build.gradle18
-rw-r--r--benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/BenchmarksUtil.java43
-rw-r--r--benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/RecordTraceEventsBenchmark.java122
-rw-r--r--benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/StartEndSpanBenchmark.java164
-rw-r--r--build.gradle497
-rw-r--r--buildscripts/checkstyle.license15
-rw-r--r--buildscripts/checkstyle.xml277
-rw-r--r--buildscripts/codecov.yml2
-rw-r--r--buildscripts/import-control.xml252
-rw-r--r--buildscripts/kokoro/linux.cfg5
-rwxr-xr-xbuildscripts/kokoro/linux.sh23
-rw-r--r--buildscripts/kokoro/linux_build.cfg19
-rw-r--r--buildscripts/kokoro/linux_example_bazel.cfg10
-rw-r--r--buildscripts/kokoro/linux_example_format.cfg9
-rw-r--r--buildscripts/kokoro/linux_example_gradle.cfg10
-rw-r--r--buildscripts/kokoro/linux_example_license.cfg10
-rw-r--r--buildscripts/kokoro/linux_example_maven.cfg10
-rw-r--r--buildscripts/kokoro/linux_framework.cfg10
-rw-r--r--buildscripts/kokoro/linux_git_history.cfg10
-rwxr-xr-xbuildscripts/kokoro/linux_presubmit.sh90
-rw-r--r--buildscripts/kokoro/macos.cfg6
-rwxr-xr-xbuildscripts/kokoro/windows.bat21
-rw-r--r--buildscripts/kokoro/windows.cfg5
-rw-r--r--checker-framework/stubs/grpc.astub12
-rw-r--r--checker-framework/stubs/guava.astub14
-rw-r--r--checker-framework/stubs/log4j.astub8
-rw-r--r--checker-framework/stubs/org-springframework-cloud-sleuth.astub19
-rw-r--r--checker-framework/stubs/org-springframework-cloud-sleuth.log.astub9
-rw-r--r--contrib/agent/README.md95
-rw-r--r--contrib/agent/build.gradle246
-rw-r--r--contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java195
-rw-r--r--contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java144
-rw-r--r--contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java87
-rw-r--r--contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt1
-rw-r--r--contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java84
-rw-r--r--contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java89
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java68
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java97
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java77
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java74
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java54
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java103
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java65
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java111
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java25
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java23
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java78
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java41
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java108
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java39
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java108
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java65
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java41
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java107
-rw-r--r--contrib/agent/src/main/resources/reference.conf23
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java87
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java73
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java60
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java55
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java55
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java55
-rw-r--r--contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt1
-rw-r--r--contrib/appengine_standard_util/README.md35
-rw-r--r--contrib/appengine_standard_util/build.gradle52
-rw-r--r--contrib/appengine_standard_util/src/main/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtils.java98
-rw-r--r--contrib/appengine_standard_util/src/main/proto/trace_id.proto10
-rw-r--r--contrib/appengine_standard_util/src/test/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtilsTest.java161
-rw-r--r--contrib/dropwizard/README.md112
-rw-r--r--contrib/dropwizard/build.gradle17
-rw-r--r--contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardMetrics.java269
-rw-r--r--contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardUtils.java55
-rw-r--r--contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardMetricsTest.java304
-rw-r--r--contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardUtilsTest.java41
-rw-r--r--contrib/exemplar_util/README.md36
-rw-r--r--contrib/exemplar_util/build.gradle15
-rw-r--r--contrib/exemplar_util/src/main/java/io/opencensus/contrib/exemplar/util/ExemplarUtils.java79
-rw-r--r--contrib/exemplar_util/src/test/java/io/opencensus/contrib/exemplar/util/ExemplarUtilsTest.java105
-rw-r--r--contrib/grpc_metrics/README.md5
-rw-r--r--contrib/grpc_metrics/build.gradle16
-rw-r--r--contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstants.java495
-rw-r--r--contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViewConstants.java1339
-rw-r--r--contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViews.java250
-rw-r--r--contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstantsTest.java78
-rw-r--r--contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewConstantsTest.java178
-rw-r--r--contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewsTest.java121
-rw-r--r--contrib/grpc_util/README.md35
-rw-r--r--contrib/grpc_util/build.gradle26
-rw-r--r--contrib/grpc_util/src/main/java/io/opencensus/contrib/grpc/util/StatusConverter.java165
-rw-r--r--contrib/grpc_util/src/test/java/io/opencensus/contrib/grpc/util/StatusConverterTest.java92
-rw-r--r--contrib/http_util/README.md41
-rw-r--r--contrib/http_util/build.gradle16
-rw-r--r--contrib/http_util/src/main/java/io/opencensus/contrib/http/util/CloudTraceFormat.java149
-rw-r--r--contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpMeasureConstants.java175
-rw-r--r--contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpPropagationUtil.java41
-rw-r--r--contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViewConstants.java190
-rw-r--r--contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViews.java103
-rw-r--r--contrib/http_util/src/test/java/io/opencensus/contrib/http/util/CloudTraceFormatTest.java295
-rw-r--r--contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpMeasureConstantsTest.java67
-rw-r--r--contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpPropagationUtilTest.java36
-rw-r--r--contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewConstantsTest.java148
-rw-r--r--contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewsTest.java67
-rw-r--r--contrib/log_correlation/log4j2/README.md88
-rw-r--r--contrib/log_correlation/log4j2/build.gradle26
-rw-r--r--contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/ContextDataUtils.java212
-rw-r--r--contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjector.java177
-rw-r--r--contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/AbstractOpenCensusLog4jLogCorrelationTest.java97
-rw-r--r--contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationAllSpansTest.java167
-rw-r--r--contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationNoSpansTest.java86
-rw-r--r--contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationSampledSpansTest.java89
-rw-r--r--contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjectorTest.java207
-rw-r--r--contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/TestSpan.java46
-rw-r--r--contrib/log_correlation/stackdriver/README.md147
-rw-r--r--contrib/log_correlation/stackdriver/build.gradle13
-rw-r--r--contrib/log_correlation/stackdriver/src/main/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancer.java214
-rw-r--r--contrib/log_correlation/stackdriver/src/test/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancerTest.java340
-rw-r--r--contrib/monitored_resource_util/README.md34
-rw-r--r--contrib/monitored_resource_util/build.gradle15
-rw-r--r--contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtils.java137
-rw-r--r--contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/GcpMetadataConfig.java90
-rw-r--r--contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResource.java305
-rw-r--r--contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtils.java54
-rw-r--r--contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/ResourceType.java47
-rw-r--r--contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtilsTest.java57
-rw-r--r--contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceTest.java83
-rw-r--r--contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtilsTest.java42
-rw-r--r--contrib/spring/README.md160
-rw-r--r--contrib/spring/build.gradle21
-rw-r--r--contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java72
-rw-r--r--contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSqlAspect.java91
-rw-r--r--contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java58
-rw-r--r--contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Traced.java43
-rw-r--r--contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringAspectTest.java187
-rw-r--r--contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java54
-rw-r--r--contrib/spring/src/test/resources/spring.xml23
-rw-r--r--contrib/spring_sleuth_v1x/README.md52
-rw-r--r--contrib/spring_sleuth_v1x/build.gradle21
-rw-r--r--contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthAutoConfiguration.java63
-rw-r--r--contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthProperties.java42
-rw-r--r--contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpan.java79
-rw-r--r--contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolder.java152
-rw-r--r--contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracer.java329
-rw-r--r--contrib/spring_sleuth_v1x/src/main/resources/META-INF/additional-spring-configuration-metadata.json0
-rw-r--r--contrib/spring_sleuth_v1x/src/main/resources/META-INF/spring.factories7
-rw-r--r--contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolderTest.java151
-rw-r--r--contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanTest.java66
-rw-r--r--contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracerTest.java185
-rw-r--r--contrib/zpages/README.md97
-rw-r--r--contrib/zpages/build.gradle16
-rw-r--r--contrib/zpages/screenshots/rpcz-example.pngbin0 -> 73473 bytes
-rw-r--r--contrib/zpages/screenshots/statsz-example-1.pngbin0 -> 111406 bytes
-rw-r--r--contrib/zpages/screenshots/statsz-example-2.pngbin0 -> 49036 bytes
-rw-r--r--contrib/zpages/screenshots/traceconfigz-example.pngbin0 -> 79485 bytes
-rw-r--r--contrib/zpages/screenshots/tracez-example.pngbin0 -> 55304 bytes
-rw-r--r--contrib/zpages/src/main/java/io/opencensus/contrib/zpages/RpczZPageHandler.java487
-rw-r--r--contrib/zpages/src/main/java/io/opencensus/contrib/zpages/StatszZPageHandler.java629
-rw-r--r--contrib/zpages/src/main/java/io/opencensus/contrib/zpages/Style.java73
-rw-r--r--contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TraceConfigzZPageHandler.java223
-rw-r--r--contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TracezZPageHandler.java699
-rw-r--r--contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandler.java49
-rw-r--r--contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandlers.java200
-rw-r--r--contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHttpHandler.java88
-rw-r--r--contrib/zpages/src/test/java/io/opencensus/contrib/zpages/RpczZPageHandlerTest.java97
-rw-r--r--contrib/zpages/src/test/java/io/opencensus/contrib/zpages/StatszZPageHandlerTest.java279
-rw-r--r--contrib/zpages/src/test/java/io/opencensus/contrib/zpages/TracezZPageHandlerTest.java147
-rw-r--r--contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHandlersTest.java49
-rw-r--r--contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHttpHandlerTest.java42
-rw-r--r--examples/BUILD.bazel155
-rw-r--r--examples/README.md113
-rw-r--r--examples/WORKSPACE53
-rw-r--r--examples/build.gradle154
-rw-r--r--examples/gradle/wrapper/gradle-wrapper.jarbin0 -> 54417 bytes
-rw-r--r--examples/gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xexamples/gradlew172
-rw-r--r--examples/gradlew.bat84
-rw-r--r--examples/opencensus_workspace.bzl1680
-rw-r--r--examples/pom.xml169
-rw-r--r--examples/settings.gradle1
-rw-r--r--examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldClient.java151
-rw-r--r--examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldServer.java176
-rw-r--r--examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldUtils.java50
-rw-r--r--examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java111
-rw-r--r--examples/src/main/java/io/opencensus/examples/tags/TagContextExample.java77
-rw-r--r--examples/src/main/java/io/opencensus/examples/trace/MultiSpansContextTracing.java89
-rw-r--r--examples/src/main/java/io/opencensus/examples/trace/MultiSpansScopedTracing.java85
-rw-r--r--examples/src/main/java/io/opencensus/examples/trace/MultiSpansTracing.java72
-rw-r--r--examples/src/main/java/io/opencensus/examples/trace/Utils.java37
-rw-r--r--examples/src/main/java/io/opencensus/examples/zpages/ZPagesTester.java108
-rw-r--r--examples/src/main/proto/helloworld.proto39
-rw-r--r--exporters/stats/prometheus/README.md81
-rw-r--r--exporters/stats/prometheus/build.gradle19
-rw-r--r--exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java298
-rw-r--r--exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java177
-rw-r--r--exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java81
-rw-r--r--exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java326
-rw-r--r--exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java168
-rw-r--r--exporters/stats/signalfx/README.md76
-rw-r--r--exporters/stats/signalfx/build.gradle23
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java59
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java188
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java153
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java109
-rw-r--r--exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java105
-rw-r--r--exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java320
-rw-r--r--exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java90
-rw-r--r--exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java93
-rw-r--r--exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java149
-rw-r--r--exporters/stats/stackdriver/README.md171
-rw-r--r--exporters/stats/stackdriver/build.gradle30
-rw-r--r--exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java518
-rw-r--r--exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java274
-rw-r--r--exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java159
-rw-r--r--exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java363
-rw-r--r--exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java568
-rw-r--r--exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java310
-rw-r--r--exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java72
-rw-r--r--exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java129
-rw-r--r--exporters/trace/instana/README.md73
-rw-r--r--exporters/trace/instana/build.gradle16
-rw-r--r--exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java235
-rw-r--r--exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java107
-rw-r--r--exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java178
-rw-r--r--exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java54
-rw-r--r--exporters/trace/jaeger/README.md90
-rw-r--r--exporters/trace/jaeger/build.gradle37
-rw-r--r--exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java321
-rw-r--r--exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java136
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java226
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java182
-rw-r--r--exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java52
-rw-r--r--exporters/trace/logging/README.md57
-rw-r--r--exporters/trace/logging/build.gradle11
-rw-r--r--exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java81
-rw-r--r--exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java101
-rw-r--r--exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java53
-rw-r--r--exporters/trace/ocagent/README.md48
-rw-r--r--exporters/trace/ocagent/build.gradle21
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java184
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java126
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java155
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java62
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java390
-rw-r--r--exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java29
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java169
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java109
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java122
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java58
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java54
-rw-r--r--exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java357
-rw-r--r--exporters/trace/stackdriver/README.md127
-rw-r--r--exporters/trace/stackdriver/build.gradle31
-rw-r--r--exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java148
-rw-r--r--exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java118
-rw-r--r--exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java141
-rw-r--r--exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java501
-rw-r--r--exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java54
-rw-r--r--exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java53
-rw-r--r--exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java64
-rw-r--r--exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java489
-rw-r--r--exporters/trace/zipkin/README.md82
-rw-r--r--exporters/trace/zipkin/build.gradle18
-rw-r--r--exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java104
-rw-r--r--exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java215
-rw-r--r--exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java124
-rw-r--r--exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java238
-rw-r--r--exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java53
-rw-r--r--findbugs-exclude.xml106
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 54417 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xgradlew172
-rw-r--r--gradlew.bat84
-rw-r--r--impl/README.md5
-rw-r--r--impl/build.gradle21
-rw-r--r--impl/src/main/java/io/opencensus/impl/internal/DisruptorEventQueue.java241
-rw-r--r--impl/src/main/java/io/opencensus/impl/metrics/MetricsComponentImpl.java29
-rw-r--r--impl/src/main/java/io/opencensus/impl/stats/StatsComponentImpl.java31
-rw-r--r--impl/src/main/java/io/opencensus/impl/tags/TagsComponentImpl.java23
-rw-r--r--impl/src/main/java/io/opencensus/impl/trace/TraceComponentImpl.java67
-rw-r--r--impl/src/main/java/io/opencensus/impl/trace/internal/ThreadLocalRandomHandler.java35
-rw-r--r--impl/src/main/java/io/opencensus/trace/TraceComponentImpl.java68
-rw-r--r--impl/src/test/java/io/opencensus/impl/internal/DisruptorEventQueueTest.java104
-rw-r--r--impl/src/test/java/io/opencensus/impl/metrics/MetricsTest.java42
-rw-r--r--impl/src/test/java/io/opencensus/impl/stats/StatsTest.java41
-rw-r--r--impl/src/test/java/io/opencensus/impl/tags/TagsTest.java41
-rw-r--r--impl/src/test/java/io/opencensus/impl/trace/TracingTest.java53
-rw-r--r--impl_core/README.md5
-rw-r--r--impl_core/build.gradle17
-rw-r--r--impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/B3FormatImplBenchmark.java99
-rw-r--r--impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplBenchmark.java90
-rw-r--r--impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/TextFormatBenchmarkBase.java59
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/common/MillisClock.java48
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/CheckerFrameworkUtils.java33
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/CurrentState.java131
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/DaemonThreadFactory.java53
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/EventQueue.java36
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/NoopScope.java38
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/SimpleEventQueue.java32
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/TimestampConverter.java51
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/Utils.java41
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java283
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImpl.java155
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedLongGaugeImpl.java153
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/DoubleGaugeImpl.java174
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/LongGaugeImpl.java174
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/Meter.java34
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/MetricRegistryImpl.java160
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/MetricsComponentImplBase.java45
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/export/ExportComponentImpl.java31
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImpl.java64
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java95
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java66
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java138
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java194
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MetricProducerImpl.java38
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MetricUtils.java118
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java556
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java464
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/RecordUtils.java241
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java92
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java104
-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.java56
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java71
-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.java60
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java85
-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.java116
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.java68
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java190
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.java49
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java35
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/NoRecordEventsSpanImpl.java85
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/RecordEventsSpanImpl.java579
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/SpanBuilderImpl.java253
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/StartEndHandlerImpl.java127
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/TraceComponentImplBase.java90
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/TracerImpl.java52
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/config/TraceConfigImpl.java43
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/export/ExportComponentImpl.java93
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImpl.java81
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImpl.java396
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/export/RunningSpanStoreImpl.java71
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/export/SampledSpanStoreImpl.java81
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/export/SpanExporterImpl.java214
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveList.java181
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/internal/RandomHandler.java50
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/propagation/B3Format.java113
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/propagation/BinaryFormatImpl.java148
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/trace/propagation/PropagationComponentImpl.java37
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/internal/CurrentStateTest.java57
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/internal/TimestampConverterTest.java51
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/internal/UtilsTest.java47
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImplTest.java221
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedLongGaugeImplTest.java224
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/metrics/DoubleGaugeImplTest.java292
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/metrics/LongGaugeImplTest.java287
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/metrics/MetricRegistryImplTest.java356
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/metrics/MetricsComponentImplBaseTest.java53
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/metrics/export/ExportComponentImplTest.java35
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImplTest.java115
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java133
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java159
-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/MetricUtilsTest.java129
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java339
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.java34
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/RecordUtilsTest.java116
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.java77
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java349
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java232
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java1021
-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/ScopedTagContextsTest.java112
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java167
-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.java84
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.java33
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java94
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java329
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java85
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.java147
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/NoRecordEventsSpanImplTest.java91
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/RecordEventsSpanImplTest.java594
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/SpanBuilderImplTest.java404
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/TraceComponentImplBaseTest.java57
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/TracerImplTest.java61
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/config/TraceConfigImplTest.java53
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/export/ExportComponentImplTest.java55
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImplTest.java168
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImplTest.java368
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopRunningSpanStoreImplTest.java95
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopSampledSpanStoreImplTest.java118
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/export/SpanExporterImplTest.java233
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveListTest.java123
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/propagation/B3FormatTest.java221
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplTest.java191
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/trace/propagation/PropagationComponentImplTest.java40
-rw-r--r--impl_lite/README.md6
-rw-r--r--impl_lite/build.gradle12
-rw-r--r--impl_lite/src/main/java/io/opencensus/impllite/metrics/MetricsComponentImplLite.java29
-rw-r--r--impl_lite/src/main/java/io/opencensus/impllite/stats/StatsComponentImplLite.java31
-rw-r--r--impl_lite/src/main/java/io/opencensus/impllite/tags/TagsComponentImplLite.java23
-rw-r--r--impl_lite/src/main/java/io/opencensus/impllite/trace/TraceComponentImplLite.java65
-rw-r--r--impl_lite/src/main/java/io/opencensus/trace/TraceComponentImplLite.java66
-rw-r--r--impl_lite/src/test/java/io/opencensus/impllite/metrics/MetricsTest.java42
-rw-r--r--impl_lite/src/test/java/io/opencensus/impllite/stats/StatsTest.java41
-rw-r--r--impl_lite/src/test/java/io/opencensus/impllite/tags/TagsTest.java41
-rw-r--r--impl_lite/src/test/java/io/opencensus/impllite/trace/TraceComponentImplLiteTest.java52
-rw-r--r--scripts/check-git-history.py51
-rwxr-xr-xscripts/travis_script78
-rw-r--r--settings.gradle79
-rw-r--r--testing/README.md5
-rw-r--r--testing/build.gradle11
-rw-r--r--testing/src/main/java/io/opencensus/testing/common/TestClock.java104
-rw-r--r--testing/src/main/java/io/opencensus/testing/export/TestHandler.java77
-rw-r--r--testing/src/test/java/io/opencensus/testing/common/TestClockTest.java65
628 files changed, 76952 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE
new file mode 100644
index 00000000..7d484c62
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE
@@ -0,0 +1,16 @@
+Please answer these questions before submitting a bug report.
+
+### What version of OpenCensus are you using?
+
+
+### What JVM are you using (`java -version`)?
+
+
+### What did you do?
+If possible, provide a recipe for reproducing the error.
+
+
+### What did you expect to see?
+
+
+### What did you see instead? \ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..b5caa381
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,46 @@
+# Gradle
+build
+gradle.properties
+.gradle
+local.properties
+out/
+
+# Protobuf
+gen_gradle
+
+# Bazel
+bazel-*
+
+# Maven (proto)
+target
+
+# IntelliJ IDEA
+.idea
+*.iml
+.editorconfig
+
+# Eclipse
+.classpath
+.project
+.settings
+bin
+
+# NetBeans
+/.nb-gradle
+/.nb-gradle-properties
+
+# VS Code
+.vscode
+
+# OS X
+.DS_Store
+
+# Emacs
+*~
+\#*\#
+
+# Vim
+.swp
+
+# Other
+TAGS
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.gitmodules
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..996d4c01
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,90 @@
+sudo: false
+
+language: java
+
+matrix:
+ fast_finish: true
+ include:
+ - jdk: openjdk7
+ env: TASK=BUILD
+ os: linux
+
+ - jdk: oraclejdk8
+ env: TASK=BUILD
+ os: linux
+ addons:
+ apt:
+ packages:
+ # Install the JREs that are used for integration tests in
+ # contrib/agent, but are not installed by default.
+ - openjdk-6-jdk
+
+ # - jdk: oraclejdk9
+ # env: TASK=BUILD
+ # os: linux
+
+ - jdk: oraclejdk8
+ env: TASK=CHECKER_FRAMEWORK
+ os: linux
+
+ - env: TASK=CHECK_GIT_HISTORY
+ os: linux
+
+ # Build example projects last, since they are affected by fewer pull requests.
+ - jdk: oraclejdk8
+ env: TASK=CHECK_EXAMPLES_LICENSE
+ os: linux
+
+ - jdk: oraclejdk8
+ env: TASK=BUILD_EXAMPLES_GRADLE
+ os: linux
+
+ - jdk: oraclejdk8
+ env: TASK=BUILD_EXAMPLES_MAVEN
+ os: linux
+
+ - jdk: oraclejdk8
+ env: TASK=BUILD_EXAMPLES_BAZEL
+ os: linux
+
+ - jdk: oraclejdk8
+ env: TASK=CHECK_EXAMPLES_FORMAT
+ os: linux
+
+ # Work around https://github.com/travis-ci/travis-ci/issues/2317
+ - env: TASK=BUILD
+ os: osx
+
+ allow_failures:
+ # Allowing failures because osx builds are very slow.
+ - env: TASK=BUILD
+ os: osx
+
+before_install:
+ - git log --oneline --decorate --graph -30
+ - if \[ "$TASK" == "BUILD_EXAMPLES_BAZEL" \]; then
+ echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list ;
+ curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - ;
+ sudo apt-get update ;
+ sudo apt-get install bazel ;
+ fi
+
+# Skip Travis' default Gradle install step. See http://stackoverflow.com/a/26575080.
+install: true
+
+script:
+ - scripts/travis_script
+
+after_success:
+ - if \[ "$TASK" == "BUILD" \] && \[ "$TRAVIS_JDK_VERSION" == "oraclejdk8" \] && \[ "$TRAVIS_OS_NAME" = linux \]; then
+ bash <(curl -s https://codecov.io/bash) ;
+ fi
+
+before_cache:
+ - rm -fr $HOME/.gradle/caches/modules-2/modules-2.lock
+
+cache:
+ directories:
+ - $HOME/.gradle
+ - $HOME/.gradle/caches/
+ - $HOME/.gradle/wrapper/
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 00000000..e068e731
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Google Inc. \ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..352c2419
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,150 @@
+## Unreleased
+- Add `AttributeValueDouble` to `AttributeValue`.
+- Add `createWithSender` to `JaegerTraceExporter` to allow use of `HttpSender`
+ with extra configurations.
+- Add an API `Functions.returnToString()`.
+- Migrate to new Stackdriver Kubernetes monitored resource. This could be a breaking change
+ if you are using `gke_container` resources. For more info,
+ https://cloud.google.com/monitoring/kubernetes-engine/migration#incompatible
+- Add OpenCensus Java OC-Agent Trace Exporter.
+
+## 0.16.1 - 2018-09-18
+- Fix ClassCastException in Log4j log correlation
+ ([#1436](https://github.com/census-instrumentation/opencensus-java/issues/1436)).
+- Allow users to report metrics for their registered domain (using custom prefix). This could be a
+ breaking change if you have custom prefix without (registered) domain.
+
+## 0.16.0 - 2018-09-14
+- Add APIs to register gRPC client and server views separately.
+- Add an API MeasureMap.putAttachment() for recording exemplars.
+- Add Exemplar class and an API to get Exemplar list to DistributionData.
+- Improve the styling of Rpcz, Statsz, Tracez, and Traceconfigz pages.
+- Add an artifact `opencensus-contrib-exemplar-util` that has helper utilities
+ on recording exemplars.
+- Reduce the default limit on `Link`s per `Span` to 32 (was 128 before).
+- Add Spring support for `@Traced` annotation and java.sql.PreparedStatements
+ tracing.
+- Allow custom prefix for Stackdriver metrics in `StackdriverStatsConfiguration`.
+- Add support to handle the Tracestate in the SpanContext.
+- Remove global synchronization from the get current stats state.
+- Add get/from{Byte} methods on TraceOptions and deprecate get/from{Bytes}.
+- Add an API to `StackdriverTraceConfiguration` to allow setting a
+ `TraceServiceStub` instance to be used for export RPC calls.
+- Add an experimental artifact, `opencensus-contrib-log-correlation-log4j2`, for
+ adding tracing data to Log4j 2 LogEvents.
+
+## 0.15.1 - 2018-08-28
+- Improve propagation performance by avoiding doing string formatting when calling checkArgument.
+
+## 0.15.0 - 2018-06-20
+- Expose the factory methods of MonitoredResource.
+- Add an experimental artifact, `opencensus-contrib-log-correlation-stackdriver`, for
+ correlating traces and logs with Stackdriver Logging.
+
+## 0.14.0 - 2018-06-04
+- Adds Tracing.getExportComponent().shutdown() for use within application shutdown hooks.
+- `Duration.create` now throws an `IllegalArgumentException` instead of
+ returning a zero `Duration` when the arguments are invalid.
+- `Timestamp.create` now throws an `IllegalArgumentException` instead of
+ returning a zero `Timestamp` when the arguments are invalid.
+- Remove namespace and help message prefix for Prometheus exporter. This could be
+ a breaking change if you have Prometheus metrics from OpenCensus Prometheus exporter
+ of previous versions, please point to the new metrics with no namespace instead.
+- Add an util artifact `opencensus-contrib-appengine-standard-util` to interact with the AppEngine
+ CloudTraceContext.
+- Add support for Span kinds. (fix [#1054](https://github.com/census-instrumentation/opencensus-java/issues/1054)).
+- Add client/server started_rpcs measures and views to RPC constants.
+
+## 0.13.2 - 2018-05-08
+- Map http attributes to Stackdriver format (fix [#1153](https://github.com/census-instrumentation/opencensus-java/issues/1153)).
+
+## 0.13.1 - 2018-05-02
+- Fix a typo on displaying Aggregation Type for a View on StatsZ page.
+- Set bucket bounds as "le" labels for Prometheus Stats exporter.
+
+## 0.13.0 - 2018-04-27
+- Support building with Java 9.
+- Add a QuickStart example.
+- Remove extraneous dependencies from the Agent's `pom.xml`.
+- Deprecate `Window` and `WindowData`.
+- Add a configuration class to the Prometheus stats exporter.
+- Fix build on platforms that are not supported by `netty-tcnative`.
+- Add Jaeger trace exporter.
+- Add a gRPC Hello World example.
+- Remove usages of Guava collections in `opencensus-api`.
+- Set unit "1" when the aggregation type is Count.
+- Auto detect GCE and GKE Stackdriver MonitoredResources.
+- Make Error Prone and FindBugs annotations `compileOnly` dependencies.
+- Deprecate `Mean` and `MeanData`.
+- Sort `TagKey`s in `View.create(...)`.
+- Add utility class to expose default HTTP measures, tags and view, and register
+ default views.
+- Add new RPC measure and view constants, deprecate old ones.
+- Makes the trace and span ID fields mandatory in binary format.
+- Auto detect AWS EC2 resources.
+- Add `Duration.toMillis()`.
+- Make monitored resource utils a separate artifact `opencensus-contrib-monitored-resource-util`,
+ so that it can be reused across exporters.
+- Add `LastValue`, `LastValueDouble` and `LastValueLong`. Also support them in
+ stats exporters and zpages. Please note that there is an API breaking change
+ in methods `Aggregation.match()` and `AggregationData.match()`.
+
+## 0.12.3 - 2018-04-13
+- Substitute non-ascii characters in B3Format header key.
+
+## 0.12.2 - 2018-02-26
+- Upgrade disruptor to include the fix for SleepingWaitStrategy causing 100%
+ CPU.
+
+## 0.12.1 - 2018-02-26
+- Fix performance issue where unused objects were referenced by the Disruptor.
+- Fix synchonization issue in the use of the Disruptor.
+
+## 0.12.0 - 2018-02-16
+- Rename trace exporters that have inconsistent naming. Exporters with legacy
+ names are deprecated.
+- Fixed bug in CloudTraceFormat that made it impossible to use short span id's.
+- Add `since` Javadoc tag to all APIs.
+- Add a configuration class to create StackdriverTraceExporter.
+- Add MessageEvent and deprecate NetworkEvent.
+- Instana Trace Exporter.
+- Prometheus Stats Exporter.
+- Stats Zpages: RpcZ and StatsZ.
+- Dependency updates.
+
+## 0.11.1 - 2018-01-23
+- Fixed bug that made it impossible to use short span id's (#950).
+
+## 0.11.0 - 2018-01-19
+- Add TextFormat API and two implementations (B3Format and CloudTraceFormat).
+- Add helper class to configure and create StackdriverStatsExporter.
+- Add helper methods in tracer to wrap Runnable and Callbacks and to run them.
+- Increase trace exporting interval to 5s.
+- Add helper class to register views.
+- Make stackdriver stats exporter compatible with GAE Java7.
+- Add SignalFX stats exporter.
+- Add http propagation APIs.
+- Dependency updates.
+
+## 0.10.0 - 2017-12-04
+- Add NoopRunningSpanStore and NoopSampledSpanStore.
+- Change the message event to include (un)compressed sizes for Tracez Zpage.
+- Use AppEngine compatible way to create threads.
+- Add new factory methods that support setting custom Stackdriver
+ MonitoredResource for Stackdriver Stats Exporter.
+- Dependency updates.
+
+## 0.9.1 - 2017-11-29
+- Fix several implementation bugs in Stackdriver Stats Exporter (#830, #831,
+ etc.).
+- Update length limit for View.Name to 255 (previously it's 256).
+
+## 0.9.0 - 2017-11-17
+- Initial stats and tagging implementation for Java (`impl`) and Android
+ (`impl-lite`). This implements all the stats and tagging APIs since v0.8.0.
+- Deprecate Tags.setState and Stats.setState.
+- Add a setStatus method in the Span.
+- OpenCensus Stackdriver Stats Exporter.
+- OpenCensus Stackdriver Trace Exporter is updated to use Stackdriver Trace V2
+ APIs.
+- Dependency updates.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..91279cc7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,158 @@
+# How to submit a bug report
+
+If you received an error message, please include it and any exceptions.
+
+We commonly need to know what platform you are on:
+
+* JDK/JRE version (i.e., `java -version`)
+* Operating system (i.e., `uname -a`)
+
+# How to contribute
+
+We definitely welcome patches and contributions to OpenCensus! Here are
+some guidelines and information about how to do so.
+
+## Before getting started
+
+In order to protect both you and ourselves, you will need to sign the
+[Contributor License Agreement](https://cla.developers.google.com/clas).
+
+[Eclipse](https://google-styleguide.googlecode.com/svn/trunk/eclipse-java-google-style.xml)
+and
+[IntelliJ](https://google-styleguide.googlecode.com/svn/trunk/intellij-java-google-style.xml)
+style configurations are commonly useful. For IntelliJ 14, copy the style to
+`~/.IdeaIC14/config/codestyles/`, start IntelliJ, go to File > Settings > Code
+Style, and set the Scheme to `GoogleStyle`.
+
+## Style
+We follow the [Google Java Style
+Guide](https://google.github.io/styleguide/javaguide.html). Our
+build automatically will provide warnings for simple style issues.
+
+Run the following command to format all files. This formatter uses
+[google-java-format](https://github.com/google/google-java-format):
+
+### OS X or Linux
+
+`./gradlew goJF`
+
+### Windows
+
+`gradlew.bat goJF`
+
+We also follow these project-specific guidelines:
+
+### Javadoc
+
+* All public classes and their public and protected methods MUST have javadoc.
+ It MUST be complete (all params documented etc.) Everything else
+ (package-protected classes, private) MAY have javadoc, at the code writer's
+ whim. It does not have to be complete, and reviewers are not allowed to
+ require or disallow it.
+* Each API element should have a `@since` tag specifying the minor version when
+ it was released (or the next minor version).
+* There MUST be NO javadoc errors.
+* See
+ [section 7.3.1](https://google.github.io/styleguide/javaguide.html#s7.3.1-javadoc-exception-self-explanatory)
+ in the guide for exceptions to the Javadoc requirement.
+* Reviewers may request documentation for any element that doesn't require
+ Javadoc, though the style of documentation is up to the author.
+* Try to do the least amount of change when modifying existing documentation.
+ Don't change the style unless you have a good reason.
+
+### AutoValue
+
+* Use [AutoValue](https://github.com/google/auto/tree/master/value), when
+ possible, for any new value classes. Remember to add package-private
+ constructors to all AutoValue classes to prevent classes in other packages
+ from extending them.
+
+## Building opencensus-java
+
+Continuous integration builds the project, runs the tests, and runs multiple
+types of static analysis.
+
+Run the following commands to build, run tests and most static analysis, and
+check formatting:
+
+### OS X or Linux
+
+`./gradlew clean assemble check verGJF`
+
+### Windows
+
+`gradlew.bat clean assemble check verGJF`
+
+Use these commands to run Checker Framework null analysis:
+
+### OS X or Linux
+
+`./gradlew clean assemble -PcheckerFramework`
+
+### Windows
+
+`gradlew.bat clean assemble -PcheckerFramework`
+
+### Checker Framework null analysis
+
+OpenCensus uses the [Checker Framework](https://checkerframework.org/) to
+prevent NullPointerExceptions. Since the project uses Java 6, and Java 6 doesn't
+allow annotations on types, all Checker Framework type annotations must be
+[put in comments](https://checkerframework.org/manual/#backward-compatibility).
+Putting all Checker Framework annotations and imports in comments also avoids a
+dependency on the Checker Framework library.
+
+OpenCensus uses `org.checkerframework.checker.nullness.qual.Nullable` for all
+nullable annotations on types, since `javax.annotation.Nullable` cannot be
+applied to types. However, it uses `javax.annotation.Nullable` in API method
+signatures whenever possible, so that the annotations can be uncommented and
+be included in .class files and Javadocs.
+
+### Checkstyle import control
+
+This project uses Checkstyle to specify the allowed dependencies between
+packages, using its ImportControl feature
+(http://checkstyle.sourceforge.net/config_imports.html#ImportControl).
+`buildscripts/import-control.xml` specifies the allowed imports and contains
+some guidelines on OpenCensus' inter-package dependencies. An error messsage
+such as
+`Disallowed import - edu.umd.cs.findbugs.annotations.SuppressFBWarnings. [ImportControl]`
+could mean that `import-control.xml` needs to be updated.
+
+## Benchmarks
+
+### Invoke all benchmarks on a sub-project
+
+```bash
+$ ./gradlew clean :opencensus-impl-core:jmh
+```
+
+### Invoke on a single benchmark class
+
+```bash
+./gradlew -PjmhIncludeSingleClass=BinaryFormatImplBenchmark clean :opencensus-impl-core:jmh
+```
+
+### Debug compilation errors
+When you make incompatible changes in the Benchmarks classes you may get compilation errors which
+are related to the old code not being compatible with the new code. Some of the reasons are:
+* Any plugin cannot delete the generated code (jmh generates code) because if the user configured
+the directory as the same as source code the plugin will delete users source code.
+* After you run jmh, a gradle daemon will stay alive which may cache the generated code in memory
+and use use that generated code even if the files were changed. This is an issue for classes
+generated with auto-value.
+
+Run this commands to clean the Gradle's cache:
+```bash
+./gradlew --stop
+rm -fr .gradle/
+rm -fr benchmarks/build
+```
+
+## Proposing changes
+
+Create a Pull Request with your changes. Please add any user-visible changes to
+CHANGELOG.md. The continuous integration build will run the tests and static
+analysis. It will also check that the pull request branch has no merge commits.
+When the changes are accepted, they will be merged or cherry-picked by an
+OpenCensus core developer.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..d6456956
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..0859e7db
--- /dev/null
+++ b/README.md
@@ -0,0 +1,306 @@
+# OpenCensus - A stats collection and distributed tracing framework
+[![Gitter chat][gitter-image]][gitter-url]
+[![Maven Central][maven-image]][maven-url]
+[![Javadocs][javadoc-image]][javadoc-url]
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Coverage Status][codecov-image]][codecov-url]
+
+
+OpenCensus is a toolkit for collecting application performance and behavior data. It currently
+includes 3 apis: stats, tracing and tags.
+
+The library is in [Beta](#versioning) stage and APIs are expected to be mostly stable. The
+library is expected to move to [GA](#versioning) stage after v1.0.0 major release.
+
+Please join [gitter](https://gitter.im/census-instrumentation/Lobby) for help or feedback on this
+project.
+
+## OpenCensus Quickstart for Libraries
+
+Integrating OpenCensus with a new library means recording stats or traces and propagating context.
+For application integration please see [Quickstart for Applications](https://github.com/census-instrumentation/opencensus-java#quickstart-for-applications).
+
+The full quick start example can also be found on the [OpenCensus website](https://opencensus.io/java/index.html).
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+```
+
+For Bazel add the following lines to the WORKSPACE file:
+```
+maven_jar(
+ name = "io_opencensus_opencensus_api",
+ artifact = "io.opencensus:opencensus-api:0.15.0",
+ sha1 = "9a098392b287d7924660837f4eba0ce252013683",
+)
+```
+Then targets can specify `@io_opencensus_opencensus_api//jar` as a dependency to depend on this jar:
+```bazel
+deps = [
+ "@io_opencensus_opencensus_api//jar",
+]
+```
+You may also need to import the transitive dependencies. See [generate external dependencies from
+Maven projects](https://docs.bazel.build/versions/master/generate-workspace.html).
+
+### Hello "OpenCensus" trace events
+
+Here's an example of creating a Span and record some trace annotations. Notice that recording the
+annotations is possible because we propagate scope. 3rd parties libraries like SLF4J can integrate
+the same way.
+
+```java
+import io.opencensus.common.Scope;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+
+public final class MyClassWithTracing {
+ private static final Tracer tracer = Tracing.getTracer();
+
+ public static void doWork() {
+ // Create a child Span of the current Span. Always record events for this span and force it to
+ // be sampled. This makes it easier to try out the example, but unless you have a clear use
+ // case, you don't need to explicitly set record events or sampler.
+ try (Scope ss =
+ tracer
+ .spanBuilder("MyChildWorkSpan")
+ .setRecordEvents(true)
+ .setSampler(Samplers.alwaysSample())
+ .startScopedSpan()) {
+ doInitialWork();
+ tracer.getCurrentSpan().addAnnotation("Finished initial work");
+ doFinalWork();
+ }
+ }
+
+ private static void doInitialWork() {
+ // ...
+ tracer.getCurrentSpan().addAnnotation("Important.");
+ // ...
+ }
+
+ private static void doFinalWork() {
+ // ...
+ tracer.getCurrentSpan().addAnnotation("More important.");
+ // ...
+ }
+}
+```
+
+### Hello "OpenCensus" stats events
+
+Here's an example on
+ * defining TagKey, Measure and View,
+ * registering a view,
+ * putting TagKey and TagValue into a scoped TagContext,
+ * recording stats against current TagContext,
+ * getting ViewData.
+
+
+For the complete example, see
+[here](https://github.com/census-instrumentation/opencensus-java/blob/master/examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java).
+
+```java
+import io.opencensus.common.Scope;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.Tags;
+import java.util.Arrays;
+import java.util.Collections;
+
+public final class MyClassWithStats {
+ private static final Tagger tagger = Tags.getTagger();
+ private static final ViewManager viewManager = Stats.getViewManager();
+ private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
+
+ // frontendKey allows us to break down the recorded data
+ private static final TagKey FRONTEND_KEY = TagKey.create("myorg_keys_frontend");
+
+ // videoSize will measure the size of processed videos.
+ private static final MeasureLong VIDEO_SIZE =
+ MeasureLong.create("my.org/measure/video_size", "size of processed videos", "By");
+
+ // Create view to see the processed video size distribution broken down by frontend.
+ // The view has bucket boundaries (0, 256, 65536) that will group measure values into
+ // histogram buckets.
+ private static final View.Name VIDEO_SIZE_VIEW_NAME = View.Name.create("my.org/views/video_size");
+ private static final View VIDEO_SIZE_VIEW =
+ View.create(
+ VIDEO_SIZE_VIEW_NAME,
+ "processed video size over time",
+ VIDEO_SIZE,
+ Aggregation.Distribution.create(
+ BucketBoundaries.create(Arrays.asList(0.0, 256.0, 65536.0))),
+ Collections.singletonList(FRONTEND_KEY));
+
+ public static void initialize() {
+ // ...
+ viewManager.registerView(VIDEO_SIZE_VIEW);
+ }
+
+ public static void processVideo() {
+ try (Scope scopedTags =
+ tagger
+ .currentBuilder()
+ .put(FRONTEND_KEY, TagValue.create("mobile-ios9.3.5"))
+ .buildScoped()) {
+ // Processing video.
+ // ...
+
+ // Record the processed video size.
+ statsRecorder.newMeasureMap().put(VIDEO_SIZE, 25648).record();
+ }
+ }
+
+ public static void printStats() {
+ ViewData viewData = viewManager.getView(VIDEO_SIZE_VIEW_NAME);
+ System.out.println(
+ String.format("Recorded stats for %s:\n %s", VIDEO_SIZE_VIEW_NAME.asString(), viewData));
+ }
+}
+```
+
+## OpenCensus Quickstart for Applications
+
+Besides recording tracing/stats events the application also need to link the implementation,
+setup exporters, and debugging [Z-Pages](https://github.com/census-instrumentation/opencensus-java/tree/master/contrib/zpages).
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+For Bazel add the following lines to the WORKSPACE file:
+```
+maven_jar(
+ name = "io_opencensus_opencensus_api",
+ artifact = "io.opencensus:opencensus-api:0.15.0",
+ sha1 = "9a098392b287d7924660837f4eba0ce252013683",
+)
+
+maven_jar(
+ name = "io_opencensus_opencensus_impl_core",
+ artifact = "io.opencensus:opencensus-impl-core:0.15.0",
+ sha1 = "36c775926ba1e54af7c37d0503cfb99d986f6229",
+)
+
+maven_jar(
+ name = "io_opencensus_opencensus_impl",
+ artifact = "io.opencensus:opencensus-impl:0.15.0",
+ sha1 = "d7bf0d7ee5a0594f840271c11c9f8d6f754f35d6",
+)
+```
+Then add the following lines to BUILD.bazel file:
+```bazel
+deps = [
+ "@io_opencensus_opencensus_api//jar",
+]
+runtime_deps = [
+ "@io_opencensus_opencensus_impl_core//jar",
+ "@io_opencensus_opencensus_impl//jar",
+]
+```
+Again you may need to import the transitive dependencies. See [generate external dependencies from
+Maven projects](https://docs.bazel.build/versions/master/generate-workspace.html).
+
+### How to setup exporters?
+
+#### Trace exporters
+* [Instana][TraceExporterInstana]
+* [Jaeger][TraceExporterJaeger]
+* [Logging][TraceExporterLogging]
+* [Stackdriver][TraceExporterStackdriver]
+* [Zipkin][TraceExporterZipkin]
+
+#### Stats exporters
+* [Stackdriver][StatsExporterStackdriver]
+* [SignalFx][StatsExporterSignalFx]
+* [Prometheus][StatsExporterPrometheus]
+
+### How to setup debugging Z-Pages?
+
+If the application owner wants to export in-process tracing and stats data via HTML debugging pages
+see this [link](https://github.com/census-instrumentation/opencensus-java/tree/master/contrib/zpages#quickstart).
+
+## Versioning
+
+This library follows [Semantic Versioning][semver].
+
+**GA**: Libraries defined at a GA quality level are stable, and will not introduce
+backwards-incompatible changes in any minor or patch releases. We will address issues and requests
+with the highest priority. If we were to make a backwards-incompatible changes on an API, we will
+first mark the existing API as deprecated and keep it for 18 months before removing it.
+
+**Beta**: Libraries defined at a Beta quality level are expected to be mostly stable and we're
+working towards their release candidate. We will address issues and requests with a higher priority.
+There may be backwards incompatible changes in a minor version release, though not in a patch
+release. If an element is part of an API that is only meant to be used by exporters or other
+opencensus libraries, then there is no deprecation period. Otherwise, we will deprecate it for 18
+months before removing it, if possible.
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[javadoc-image]: https://www.javadoc.io/badge/io.opencensus/opencensus-api.svg
+[javadoc-url]: https://www.javadoc.io/doc/io.opencensus/opencensus-api
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-api/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-api
+[gitter-image]: https://badges.gitter.im/census-instrumentation/lobby.svg
+[gitter-url]: https://gitter.im/census-instrumentation/lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+[codecov-image]: https://codecov.io/gh/census-instrumentation/opencensus-java/branch/master/graph/badge.svg
+[codecov-url]: https://codecov.io/gh/census-instrumentation/opencensus-java/branch/master/
+[semver]: http://semver.org/
+[TraceExporterInstana]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/instana#quickstart
+[TraceExporterJaeger]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/jaeger#quickstart
+[TraceExporterLogging]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/logging#quickstart
+[TraceExporterStackdriver]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/stackdriver#quickstart
+[TraceExporterZipkin]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/trace/zipkin#quickstart
+[StatsExporterStackdriver]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/stats/stackdriver#quickstart
+[StatsExporterSignalFx]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/stats/signalfx#quickstart
+[StatsExporterPrometheus]: https://github.com/census-instrumentation/opencensus-java/tree/master/exporters/stats/prometheus#quickstart
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 00000000..649ac81f
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,294 @@
+# How to Create a Release of OpenCensus Java (for Maintainers Only)
+
+## Build Environments
+
+We deploy OpenCensus Java to Maven Central under the following systems:
+
+- Ubuntu 14.04
+
+Other systems may also work, but we haven't verified them.
+
+## Prerequisites
+
+### Setup OSSRH and Signing
+
+If you haven't deployed artifacts to Maven Central before, you need to setup
+your OSSRH (OSS Repository Hosting) account and signing keys.
+
+- Follow the instructions on [this
+ page](http://central.sonatype.org/pages/ossrh-guide.html) to set up an
+ account with OSSRH.
+ - You only need to create the account, not set up a new project
+ - Contact a OpenCensus Java maintainer to add your account after you
+ have created it.
+- (For release deployment only) [Install
+ GnuPG](http://central.sonatype.org/pages/working-with-pgp-signatures.html#installing-gnupg)
+ and [generate your key
+ pair](http://central.sonatype.org/pages/working-with-pgp-signatures.html#generating-a-key-pair).
+ You'll also need to [publish your public
+ key](http://central.sonatype.org/pages/working-with-pgp-signatures.html#distributing-your-public-key)
+ to make it visible to the Sonatype servers.
+- Put your GnuPG key password and OSSRH account information in
+ `<your-home-directory>/.gradle/gradle.properties`:
+
+ ```
+ # You need the signing properties only if you are making release deployment
+ signing.keyId=<8-character-public-key-id>
+ signing.password=<key-password>
+ signing.secretKeyRingFile=<your-home-directory>/.gnupg/secring.gpg
+
+ ossrhUsername=<ossrh-username>
+ ossrhPassword=<ossrh-password>
+ checkstyle.ignoreFailures=false
+ ```
+
+## Tagging the Release
+
+The first step in the release process is to create a release branch, bump
+versions, and create a tag for the release. Our release branches follow the
+naming convention of `v<major>.<minor>.x`, while the tags include the patch
+version `v<major>.<minor>.<patch>`. For example, the same branch `v0.4.x` would
+be used to create all `v0.4` tags (e.g. `v0.4.0`, `v0.4.1`).
+
+In this section upstream repository refers to the main opencensus-java github
+repository.
+
+Before any push to the upstream repository you need to create a [personal access
+token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/).
+
+1. Create the release branch and push it to GitHub:
+
+ ```bash
+ $ MAJOR=0 MINOR=4 PATCH=0 # Set appropriately for new release
+ $ VERSION_FILES=(
+ build.gradle
+ examples/build.gradle
+ examples/pom.xml
+ api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java
+ exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java
+ )
+ $ git checkout -b v$MAJOR.$MINOR.x master
+ $ git push upstream v$MAJOR.$MINOR.x
+ ```
+ The branch will be automatically protected by the GitHub branch protection rule for release
+ branches.
+
+2. For `master` branch:
+
+ - Change root build files to the next minor snapshot (e.g.
+ `0.5.0-SNAPSHOT`).
+
+ ```bash
+ $ git checkout -b bump-version master
+ # Change version to next minor (and keep -SNAPSHOT)
+ $ sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*CURRENT_OPENCENSUS_VERSION\)/'$MAJOR.$((MINOR+1)).0'\1/' \
+ "${VERSION_FILES[@]}"
+ $ ./gradlew build
+ $ git commit -a -m "Start $MAJOR.$((MINOR+1)).0 development cycle"
+ ```
+
+ - Go through PR review and push the master branch to GitHub:
+
+ ```bash
+ $ git checkout master
+ $ git merge --ff-only bump-version
+ $ git push upstream master
+ ```
+
+3. For `vMajor.Minor.x` branch:
+
+ - Change root build files to remove "-SNAPSHOT" for the next release
+ version (e.g. `0.4.0`). Commit the result and make a tag:
+
+ ```bash
+ $ git checkout -b release v$MAJOR.$MINOR.x
+ # Change version to remove -SNAPSHOT
+ $ sed -i 's/-SNAPSHOT\(.*CURRENT_OPENCENSUS_VERSION\)/\1/' "${VERSION_FILES[@]}"
+ $ ./gradlew build
+ $ git commit -a -m "Bump version to $MAJOR.$MINOR.$PATCH"
+ $ git tag -a v$MAJOR.$MINOR.$PATCH -m "Version $MAJOR.$MINOR.$PATCH"
+ ```
+
+ - Change root build files to the next snapshot version (e.g.
+ `0.4.1-SNAPSHOT`). Commit the result:
+
+ ```bash
+ # Change version to next patch and add -SNAPSHOT
+ $ sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*CURRENT_OPENCENSUS_VERSION\)/'$MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT'\1/' \
+ "${VERSION_FILES[@]}"
+ $ ./gradlew build
+ $ git commit -a -m "Bump version to $MAJOR.$MINOR.$((PATCH+1))-SNAPSHOT"
+ ```
+
+ - Go through PR review and push the release tag and updated release branch
+ to GitHub:
+
+ ```bash
+ $ git checkout v$MAJOR.$MINOR.x
+ $ git merge --ff-only release
+ $ git push upstream v$MAJOR.$MINOR.$PATCH
+ $ git push upstream v$MAJOR.$MINOR.x
+ ```
+
+## Deployment
+
+Deployment to Maven Central (or the snapshot repo) is for all of the artifacts
+from the project.
+
+### Branch
+
+Before building/deploying, be sure to switch to the appropriate tag. The tag
+must reference a commit that has been pushed to the main repository, i.e., has
+gone through code review. For the current release use:
+
+```bash
+$ git checkout -b v$MAJOR.$MINOR.$PATCH tags/v$MAJOR.$MINOR.$PATCH
+```
+
+### Initial Deployment
+
+The following command will build the whole project and upload it to Maven
+Central. Parallel building [is not safe during
+uploadArchives](https://issues.gradle.org/browse/GRADLE-3420).
+
+```bash
+$ ./gradlew clean build && ./gradlew -Dorg.gradle.parallel=false uploadArchives
+```
+
+If the version has the `-SNAPSHOT` suffix, the artifacts will automatically go
+to the snapshot repository. Otherwise it's a release deployment and the
+artifacts will go to a staging repository.
+
+When deploying a Release, the deployment will create [a new staging
+repository](https://oss.sonatype.org/#stagingRepositories). You'll need to look
+up the ID in the OSSRH UI (usually in the form of `opencensus-*`).
+
+## Releasing on Maven Central
+
+Once all of the artifacts have been pushed to the staging repository, the
+repository must first be `closed`, which will trigger several sanity checks on
+the repository. If this completes successfully, the repository can then be
+`released`, which will begin the process of pushing the new artifacts to Maven
+Central (the staging repository will be destroyed in the process). You can see
+the complete process for releasing to Maven Central on the [OSSRH
+site](http://central.sonatype.org/pages/releasing-the-deployment.html).
+
+## Announcement
+
+Once deployment is done, go to Github [release
+page](https://github.com/census-instrumentation/opencensus-java/releases), press
+`Draft a new release` to write release notes about the new release.
+
+You can use `git log upstream/v$MAJOR.$((MINOR-1)).x..upstream/v$MAJOR.$MINOR.x --graph --first-parent`
+or the Github [compare tool](https://github.com/census-instrumentation/opencensus-java/compare/)
+to view a summary of all commits since last release as a reference. In addition, you can refer to
+[CHANGELOG.md](https://github.com/census-instrumentation/opencensus-java/blob/master/CHANGELOG.md)
+for a list of major changes since last release.
+
+Please pick major or important user-visible changes only.
+
+## Update release versions in documentations and build files
+
+After releasing is done, you need to update all readmes and examples to point to the
+latest version.
+
+1. Update README.md and gradle/maven build files on `master` branch:
+
+```bash
+$ git checkout -b bump-document-version master
+$ BUILD_FILES=(
+ examples/build.gradle
+ examples/pom.xml
+ )
+$ README_FILES=(
+ README.md
+ contrib/appengine_standard_util/README.md
+ contrib/exemplar_util/README.md
+ contrib/grpc_util/README.md
+ contrib/http_util/README.md
+ contrib/log_correlation/log4j2/README.md
+ contrib/log_correlation/stackdriver/README.md
+ contrib/monitored_resource_util/README.md
+ contrib/spring/README.md
+ contrib/spring_sleuth_v1x/README.md
+ contrib/zpages/README.md
+ exporters/stats/prometheus/README.md
+ exporters/stats/signalfx/README.md
+ exporters/stats/stackdriver/README.md
+ exporters/trace/instana/README.md
+ exporters/trace/logging/README.md
+ exporters/trace/jaeger/README.md
+ exporters/trace/ocagent/README.md
+ exporters/trace/stackdriver/README.md
+ exporters/trace/zipkin/README.md
+ )
+# Substitute versions in build files
+$ sed -i 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(.*LATEST_OPENCENSUS_RELEASE_VERSION\)/'$MAJOR.$MINOR.$PATCH'\1/' \
+ "${BUILD_FILES[@]}"
+# Substitute versions in build.gradle examples in README.md
+$ sed -i 's/\(\(compile\|runtime\).\+io\.opencensus:.\+:\)[0-9]\+\.[0-9]\+\.[0-9]\+/\1'$MAJOR.$MINOR.$PATCH'/' \
+ "${README_FILES[@]}"
+# Substitute versions in maven pom examples in README.md
+$ sed -i 's/\(<version>\)[0-9]\+\.[0-9]\+\.[0-9]\+/\1'$MAJOR.$MINOR.$PATCH'/' \
+ "${README_FILES[@]}"
+```
+
+2. Update bazel dependencies for subproject `examples`:
+
+ - Follow the instructions on [this
+ page](https://docs.bazel.build/versions/master/generate-workspace.html) to
+ install bazel migration tool. You may also need to manually apply
+ this [patch](
+ https://github.com/nevillelyh/migration-tooling/commit/f10e14fd18ad3885c7ec8aa305e4eba266a07ebf)
+ if you encounter `Unable to find a version for ... due to Invalid Range Result` error when
+ using it.
+
+ - Use the following command to generate new dependencies file:
+
+ ```bash
+ $ bazel run //generate_workspace -- \
+ --artifact=com.google.guava:guava-jdk5:23.0
+ --artifact=com.google.guava:guava:23.0 \
+ --artifact=io.grpc:grpc-all:1.9.0 \
+ --artifact=io.opencensus:opencensus-api:$MAJOR.$MINOR.$PATCH \
+ --artifact=io.opencensus:opencensus-contrib-grpc-metrics:$MAJOR.$MINOR.$PATCH \
+ --artifact=io.opencensus:opencensus-contrib-zpages:$MAJOR.$MINOR.$PATCH \
+ --artifact=io.opencensus:opencensus-exporter-stats-prometheus:$MAJOR.$MINOR.$PATCH \
+ --artifact=io.opencensus:opencensus-exporter-stats-stackdriver:$MAJOR.$MINOR.$PATCH \
+ --artifact=io.opencensus:opencensus-exporter-trace-logging:$MAJOR.$MINOR.$PATCH \
+ --artifact=io.opencensus:opencensus-exporter-trace-stackdriver:$MAJOR.$MINOR.$PATCH \
+ --artifact=io.opencensus:opencensus-impl:$MAJOR.$MINOR.$PATCH \
+ --artifact=io.prometheus:simpleclient_httpserver:0.3.0 \
+ --repositories=http://repo.maven.apache.org/maven2
+ Wrote
+ /usr/local/.../generate_workspace.runfiles/__main__/generate_workspace.bzl
+ ```
+
+ - Copy this file to overwrite `examples/opencensus_workspace.bzl`.
+
+ - Use the following command to rename the generated rules and commit the
+ changes above:
+
+ ```bash
+ $ sed -i 's/def generated_/def opencensus_/' examples/opencensus_workspace.bzl
+ $ git commit -a -m "Update release versions for all readme and build files."
+ ```
+
+3. Go through PR review and merge it to GitHub master branch.
+
+4. In addition, create a PR to mark the new release in
+[CHANGELOG.md](https://github.com/census-instrumentation/opencensus-java/blob/master/CHANGELOG.md)
+on master branch. Once that PR is merged, cherry-pick the commit and create another PR to the
+release branch (branch v$MAJOR.$MINOR.x).
+
+
+## Known Issues
+
+### Deployment for tag v0.5.0
+To rebuild the releases on the tag v0.5.0 use:
+```bash
+$ ./gradlew clean build && ./gradlew uploadArchives
+```
+
+If option `-Dorg.gradle.parallel=false` is used, you will hit [this bug](https://issues.sonatype.org/browse/OSSRH-19485)
+caused by [this bug](https://github.com/gradle/gradle/issues/1827) in gradle 3.5.
diff --git a/all/build.gradle b/all/build.gradle
new file mode 100644
index 00000000..83ffb69e
--- /dev/null
+++ b/all/build.gradle
@@ -0,0 +1,105 @@
+description = "OpenCensus All"
+
+def subprojects = [
+ project(':opencensus-api'),
+ project(':opencensus-impl-core'),
+ project(':opencensus-impl'),
+ project(':opencensus-impl-lite'),
+ project(':opencensus-testing'),
+ project(':opencensus-contrib-agent'),
+ project(':opencensus-contrib-appengine-standard-util'),
+ project(':opencensus-contrib-dropwizard'),
+ project(':opencensus-contrib-exemplar-util'),
+ project(':opencensus-contrib-grpc-util'),
+ project(':opencensus-contrib-grpc-metrics'),
+ project(':opencensus-contrib-http-util'),
+ project(':opencensus-contrib-log-correlation-log4j2'),
+ project(':opencensus-contrib-log-correlation-stackdriver'),
+ project(':opencensus-contrib-monitored-resource-util'),
+ project(':opencensus-contrib-spring'),
+ project(':opencensus-contrib-spring-sleuth-v1x'),
+ project(':opencensus-contrib-zpages'),
+ project(':opencensus-exporter-trace-logging'),
+ project(':opencensus-exporter-trace-ocagent'),
+ project(':opencensus-exporter-trace-stackdriver'),
+ project(':opencensus-exporter-trace-zipkin'),
+ project(':opencensus-exporter-trace-jaeger'),
+ project(':opencensus-exporter-stats-signalfx'),
+ project(':opencensus-exporter-stats-stackdriver'),
+ project(':opencensus-exporter-stats-prometheus'),
+]
+
+// A subset of subprojects for which we want to publish javadoc.
+def subprojects_javadoc = [
+ project(':opencensus-api'),
+ project(':opencensus-testing'),
+ project(':opencensus-contrib-agent'),
+ project(':opencensus-contrib-appengine-standard-util'),
+ project(':opencensus-contrib-dropwizard'),
+ project(':opencensus-contrib-exemplar-util'),
+ project(':opencensus-contrib-grpc-util'),
+ project(':opencensus-contrib-grpc-metrics'),
+ project(':opencensus-contrib-http-util'),
+ project(':opencensus-contrib-log-correlation-log4j2'),
+ project(':opencensus-contrib-log-correlation-stackdriver'),
+ project(':opencensus-contrib-monitored-resource-util'),
+ project(':opencensus-contrib-spring'),
+ project(':opencensus-contrib-spring-sleuth-v1x'),
+ project(':opencensus-contrib-zpages'),
+ project(':opencensus-exporter-trace-logging'),
+ project(':opencensus-exporter-trace-ocagent'),
+ project(':opencensus-exporter-trace-stackdriver'),
+ project(':opencensus-exporter-trace-zipkin'),
+ project(':opencensus-exporter-trace-jaeger'),
+ project(':opencensus-exporter-stats-signalfx'),
+ project(':opencensus-exporter-stats-stackdriver'),
+ project(':opencensus-exporter-stats-prometheus'),
+]
+
+for (subproject in rootProject.subprojects) {
+ if (subproject == project) {
+ continue
+ }
+ evaluationDependsOn(subproject.path)
+}
+
+dependencies {
+ compile subprojects
+}
+
+javadoc {
+ classpath = files(subprojects_javadoc.collect { subproject ->
+ subproject.javadoc.classpath
+ })
+ for (subproject in subprojects_javadoc) {
+ if (subproject == project) {
+ continue;
+ }
+ source subproject.javadoc.source
+ options.links subproject.javadoc.options.links.toArray(new String[0])
+ }
+ exclude 'io/opencensus/internal/**'
+}
+
+task jacocoMerge(type: JacocoMerge) {
+ dependsOn(subprojects.jacocoTestReport.dependsOn)
+ mustRunAfter(subprojects.jacocoTestReport.mustRunAfter)
+ destinationFile = file("${buildDir}/jacoco/test.exec")
+ executionData = files(subprojects.jacocoTestReport.executionData)
+ .filter { f -> f.exists() }
+}
+
+jacocoTestReport {
+ dependsOn(jacocoMerge)
+ reports {
+ xml.enabled = true
+ html.enabled = true
+ }
+
+ additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
+ sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs)
+ classDirectories = files(subprojects.sourceSets.main.output)
+ classDirectories = files(classDirectories.files.collect {
+ fileTree(dir: it)
+ })
+}
diff --git a/api/README.md b/api/README.md
new file mode 100644
index 00000000..83891d4d
--- /dev/null
+++ b/api/README.md
@@ -0,0 +1,6 @@
+OpenCensus API
+======================================================
+
+* Java 6 and Android compatible.
+* The abstract classes in this directory can be subclassed to create alternative
+ implementations of the OpenCensus library.
diff --git a/api/build.gradle b/api/build.gradle
new file mode 100644
index 00000000..31274ca0
--- /dev/null
+++ b/api/build.gradle
@@ -0,0 +1,15 @@
+description = 'OpenCensus API'
+
+dependencies {
+ compile libraries.grpc_context
+
+ compileOnly libraries.auto_value
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
+
+javadoc {
+ exclude 'io/opencensus/internal/**'
+ exclude 'io/opencensus/trace/internal/**'
+}
diff --git a/api/src/main/java/io/opencensus/common/Clock.java b/api/src/main/java/io/opencensus/common/Clock.java
new file mode 100644
index 00000000..cd311935
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Clock.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.common;
+
+/**
+ * Interface for getting the current time.
+ *
+ * @since 0.5
+ */
+public abstract class Clock {
+
+ /**
+ * Obtains the current instant from this clock.
+ *
+ * @return the current instant.
+ * @since 0.5
+ */
+ public abstract Timestamp now();
+
+ /**
+ * Returns a time measurement with nanosecond precision that can only be used to calculate elapsed
+ * time.
+ *
+ * @return a time measurement with nanosecond precision that can only be used to calculate elapsed
+ * time.
+ * @since 0.5
+ */
+ public abstract long nowNanos();
+}
diff --git a/api/src/main/java/io/opencensus/common/Duration.java b/api/src/main/java/io/opencensus/common/Duration.java
new file mode 100644
index 00000000..f46cd187
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Duration.java
@@ -0,0 +1,136 @@
+/*
+ * 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.common;
+
+import static io.opencensus.common.TimeUtils.MAX_NANOS;
+import static io.opencensus.common.TimeUtils.MAX_SECONDS;
+import static io.opencensus.common.TimeUtils.MILLIS_PER_SECOND;
+import static io.opencensus.common.TimeUtils.NANOS_PER_MILLI;
+
+import com.google.auto.value.AutoValue;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Represents a signed, fixed-length span of time represented as a count of seconds and fractions of
+ * seconds at nanosecond resolution. It is independent of any calendar and concepts like "day" or
+ * "month". Range is approximately +-10,000 years.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class Duration implements Comparable<Duration> {
+
+ /**
+ * Creates a new time duration from given seconds and nanoseconds.
+ *
+ * @param seconds Signed seconds of the span of time. Must be from -315,576,000,000 to
+ * +315,576,000,000 inclusive.
+ * @param nanos Signed fractions of a second at nanosecond resolution of the span of time.
+ * Durations less than one second are represented with a 0 `seconds` field and a positive or
+ * negative `nanos` field. For durations of one second or more, a non-zero value for the
+ * `nanos` field must be of the same sign as the `seconds` field. Must be from -999,999,999 to
+ * +999,999,999 inclusive.
+ * @return new {@code Duration} with specified fields.
+ * @throws IllegalArgumentException if the arguments are out of range or have inconsistent sign.
+ * @since 0.5
+ */
+ public static Duration create(long seconds, int nanos) {
+ if (seconds < -MAX_SECONDS) {
+ throw new IllegalArgumentException(
+ "'seconds' is less than minimum (" + -MAX_SECONDS + "): " + seconds);
+ }
+ if (seconds > MAX_SECONDS) {
+ throw new IllegalArgumentException(
+ "'seconds' is greater than maximum (" + MAX_SECONDS + "): " + seconds);
+ }
+ if (nanos < -MAX_NANOS) {
+ throw new IllegalArgumentException(
+ "'nanos' is less than minimum (" + -MAX_NANOS + "): " + nanos);
+ }
+ if (nanos > MAX_NANOS) {
+ throw new IllegalArgumentException(
+ "'nanos' is greater than maximum (" + MAX_NANOS + "): " + nanos);
+ }
+ if ((seconds < 0 && nanos > 0) || (seconds > 0 && nanos < 0)) {
+ throw new IllegalArgumentException(
+ "'seconds' and 'nanos' have inconsistent sign: seconds=" + seconds + ", nanos=" + nanos);
+ }
+ return new AutoValue_Duration(seconds, nanos);
+ }
+
+ /**
+ * Creates a new {@code Duration} from given milliseconds.
+ *
+ * @param millis the duration in milliseconds.
+ * @return a new {@code Duration} from given milliseconds.
+ * @throws IllegalArgumentException if the number of milliseconds is out of the range that can be
+ * represented by {@code Duration}.
+ * @since 0.5
+ */
+ public static Duration fromMillis(long millis) {
+ long seconds = millis / MILLIS_PER_SECOND;
+ int nanos = (int) (millis % MILLIS_PER_SECOND * NANOS_PER_MILLI);
+ return Duration.create(seconds, nanos);
+ }
+
+ /**
+ * Converts a {@link Duration} to milliseconds.
+ *
+ * @return the milliseconds representation of this {@code Duration}.
+ * @since 0.13
+ */
+ public long toMillis() {
+ return TimeUnit.SECONDS.toMillis(getSeconds()) + TimeUnit.NANOSECONDS.toMillis(getNanos());
+ }
+
+ /**
+ * Returns the number of seconds in the {@code Duration}.
+ *
+ * @return the number of seconds in the {@code Duration}.
+ * @since 0.5
+ */
+ public abstract long getSeconds();
+
+ /**
+ * Returns the number of nanoseconds in the {@code Duration}.
+ *
+ * @return the number of nanoseconds in the {@code Duration}.
+ * @since 0.5
+ */
+ public abstract int getNanos();
+
+ /**
+ * Compares this {@code Duration} to the specified {@code Duration}.
+ *
+ * @param otherDuration the other {@code Duration} to compare to, not {@code null}.
+ * @return the comparator value: zero if equal, negative if this duration is smaller than
+ * otherDuration, positive if larger.
+ * @throws NullPointerException if otherDuration is {@code null}.
+ */
+ @Override
+ public int compareTo(Duration otherDuration) {
+ int cmp = TimeUtils.compareLongs(getSeconds(), otherDuration.getSeconds());
+ if (cmp != 0) {
+ return cmp;
+ }
+ return TimeUtils.compareLongs(getNanos(), otherDuration.getNanos());
+ }
+
+ Duration() {}
+}
diff --git a/api/src/main/java/io/opencensus/common/ExperimentalApi.java b/api/src/main/java/io/opencensus/common/ExperimentalApi.java
new file mode 100644
index 00000000..7a4da7c7
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ExperimentalApi.java
@@ -0,0 +1,58 @@
+/*
+ * 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.common;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates a public API that can change at any time, and has no guarantee of API stability and
+ * backward-compatibility.
+ *
+ * <p>Usage guidelines:
+ *
+ * <ol>
+ * <li>This annotation is used only on public API. Internal interfaces should not use it.
+ * <li>After OpenCensus has gained API stability, this annotation can only be added to new API.
+ * Adding it to an existing API is considered API-breaking.
+ * <li>Removing this annotation from an API gives it stable status.
+ * </ol>
+ *
+ * @since 0.8
+ */
+@Internal
+@Retention(RetentionPolicy.SOURCE)
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.TYPE
+})
+@Documented
+public @interface ExperimentalApi {
+ /**
+ * Context information such as links to discussion thread, tracking issue etc.
+ *
+ * @since 0.8
+ */
+ String value() default "";
+}
diff --git a/api/src/main/java/io/opencensus/common/Function.java b/api/src/main/java/io/opencensus/common/Function.java
new file mode 100644
index 00000000..a9ed5a9e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Function.java
@@ -0,0 +1,38 @@
+/*
+ * 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.common;
+
+/**
+ * Used to specify matching functions for use encoding tagged unions (i.e. sum types) in Java. See
+ * {@link io.opencensus.trace.AttributeValue#match} for an example of its use.
+ *
+ * <p>Note: This class is based on the java.util.Function class added in Java 1.8. We cannot use the
+ * Function from Java 1.8 because this library is Java 1.6 compatible.
+ *
+ * @since 0.5
+ */
+public interface Function<A, B> {
+
+ /**
+ * Applies the function to the given argument.
+ *
+ * @param arg the argument to the function.
+ * @return the result of the function.
+ * @since 0.5
+ */
+ B apply(A arg);
+}
diff --git a/api/src/main/java/io/opencensus/common/Functions.java b/api/src/main/java/io/opencensus/common/Functions.java
new file mode 100644
index 00000000..ea3457ca
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Functions.java
@@ -0,0 +1,130 @@
+/*
+ * 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.common;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Commonly used {@link Function} instances.
+ *
+ * @since 0.5
+ */
+public final class Functions {
+ private Functions() {}
+
+ private static final Function<Object, /*@Nullable*/ Void> RETURN_NULL =
+ new Function<Object, /*@Nullable*/ Void>() {
+ @Override
+ @javax.annotation.Nullable
+ public Void apply(Object ignored) {
+ return null;
+ }
+ };
+
+ private static final Function<Object, Void> THROW_ILLEGAL_ARGUMENT_EXCEPTION =
+ new Function<Object, Void>() {
+ @Override
+ public Void apply(Object ignored) {
+ throw new IllegalArgumentException();
+ }
+ };
+
+ private static final Function<Object, Void> THROW_ASSERTION_ERROR =
+ new Function<Object, Void>() {
+ @Override
+ public Void apply(Object ignored) {
+ throw new AssertionError();
+ }
+ };
+
+ private static final Function<Object, /*@Nullable*/ String> RETURN_TO_STRING =
+ new Function<Object, /*@Nullable*/ String>() {
+ @Override
+ public /*@Nullable*/ String apply(Object input) {
+ return input == null ? null : input.toString();
+ }
+ };
+
+ /**
+ * A {@code Function} that always ignores its argument and returns {@code null}.
+ *
+ * @return a {@code Function} that always ignores its argument and returns {@code null}.
+ * @since 0.5
+ */
+ public static <T> Function<Object, /*@Nullable*/ T> returnNull() {
+ // It is safe to cast a producer of Void to anything, because Void is always null.
+ @SuppressWarnings("unchecked")
+ Function<Object, /*@Nullable*/ T> function = (Function<Object, /*@Nullable*/ T>) RETURN_NULL;
+ return function;
+ }
+
+ /**
+ * A {@code Function} that always ignores its argument and returns a constant value.
+ *
+ * @return a {@code Function} that always ignores its argument and returns a constant value.
+ * @since 0.5
+ */
+ public static <T> Function<Object, T> returnConstant(final T constant) {
+ return new Function<Object, T>() {
+ @Override
+ public T apply(Object ignored) {
+ return constant;
+ }
+ };
+ }
+
+ /**
+ * A {@code Function} that always returns the {@link #toString()} value of the input.
+ *
+ * @return a {@code Function} that always returns the {@link #toString()} value of the input.
+ * @since 0.17
+ */
+ public static Function<Object, /*@Nullable*/ String> returnToString() {
+ return RETURN_TO_STRING;
+ }
+
+ /**
+ * A {@code Function} that always ignores its argument and throws an {@link
+ * IllegalArgumentException}.
+ *
+ * @return a {@code Function} that always ignores its argument and throws an {@link
+ * IllegalArgumentException}.
+ * @since 0.5
+ */
+ public static <T> Function<Object, T> throwIllegalArgumentException() {
+ // It is safe to cast this function to have any return type, since it never returns a result.
+ @SuppressWarnings("unchecked")
+ Function<Object, T> function = (Function<Object, T>) THROW_ILLEGAL_ARGUMENT_EXCEPTION;
+ return function;
+ }
+
+ /**
+ * A {@code Function} that always ignores its argument and throws an {@link AssertionError}.
+ *
+ * @return a {@code Function} that always ignores its argument and throws an {@code
+ * AssertionError}.
+ * @since 0.6
+ */
+ public static <T> Function<Object, T> throwAssertionError() {
+ // It is safe to cast this function to have any return type, since it never returns a result.
+ @SuppressWarnings("unchecked")
+ Function<Object, T> function = (Function<Object, T>) THROW_ASSERTION_ERROR;
+ return function;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/common/Internal.java b/api/src/main/java/io/opencensus/common/Internal.java
new file mode 100644
index 00000000..d84fba20
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Internal.java
@@ -0,0 +1,42 @@
+/*
+ * 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.common;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates a program element (class, method, package etc) which is internal to OpenCensus, not
+ * part of the public API, and should not be used by users of the OpenCensus library.
+ *
+ * @since 0.5
+ */
+@Internal
+@Retention(RetentionPolicy.SOURCE)
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.TYPE
+})
+@Documented
+public @interface Internal {}
diff --git a/api/src/main/java/io/opencensus/common/NonThrowingCloseable.java b/api/src/main/java/io/opencensus/common/NonThrowingCloseable.java
new file mode 100644
index 00000000..30d07ac7
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/NonThrowingCloseable.java
@@ -0,0 +1,42 @@
+/*
+ * 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.common;
+
+import java.io.Closeable;
+
+/**
+ * An {@link Closeable} which cannot throw a checked exception.
+ *
+ * <p>This is useful because such a reversion otherwise requires the caller to catch the
+ * (impossible) Exception in the try-with-resources.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>
+ * try (NonThrowingAutoCloseable ctx = tryEnter()) {
+ * ...
+ * }
+ * </pre>
+ *
+ * @deprecated {@link Scope} is a better match for operations involving the current context.
+ * @since 0.5
+ */
+@Deprecated
+public interface NonThrowingCloseable extends Closeable {
+ @Override
+ void close();
+}
diff --git a/api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java b/api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java
new file mode 100644
index 00000000..3f659c12
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/OpenCensusLibraryInformation.java
@@ -0,0 +1,35 @@
+/*
+ * 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.common;
+
+/**
+ * Class holder for all common constants (such as the version) for the OpenCensus Java library.
+ *
+ * @since 0.8
+ */
+@ExperimentalApi
+public final class OpenCensusLibraryInformation {
+
+ /**
+ * The current version of the OpenCensus Java library.
+ *
+ * @since 0.8
+ */
+ public static final String VERSION = "0.17.0-SNAPSHOT"; // CURRENT_OPENCENSUS_VERSION
+
+ private OpenCensusLibraryInformation() {}
+}
diff --git a/api/src/main/java/io/opencensus/common/Scope.java b/api/src/main/java/io/opencensus/common/Scope.java
new file mode 100644
index 00000000..de954f50
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Scope.java
@@ -0,0 +1,37 @@
+/*
+ * 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.common;
+
+/**
+ * A {@link java.io.Closeable} that represents a change to the current context over a scope of code.
+ * {@link Scope#close} cannot throw a checked exception.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>
+ * try (Scope ctx = tryEnter()) {
+ * ...
+ * }
+ * </pre>
+ *
+ * @since 0.6
+ */
+@SuppressWarnings("deprecation")
+public interface Scope extends NonThrowingCloseable {
+ @Override
+ void close();
+}
diff --git a/api/src/main/java/io/opencensus/common/ServerStats.java b/api/src/main/java/io/opencensus/common/ServerStats.java
new file mode 100644
index 00000000..42efa1f2
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ServerStats.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018, 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.common;
+
+import com.google.auto.value.AutoValue;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A representation of stats measured on the server side.
+ *
+ * @since 0.16
+ */
+@Immutable
+@AutoValue
+public abstract class ServerStats {
+
+ ServerStats() {}
+
+ /**
+ * Returns Load Balancer latency, a latency observed at Load Balancer.
+ *
+ * @return Load Balancer latency in nanoseconds.
+ * @since 0.16
+ */
+ public abstract long getLbLatencyNs();
+
+ /**
+ * Returns Service latency, a latency observed at Server.
+ *
+ * @return Service latency in nanoseconds.
+ * @since 0.16
+ */
+ public abstract long getServiceLatencyNs();
+
+ /**
+ * Returns Trace options, a set of bits indicating properties of trace.
+ *
+ * @return Trace options a set of bits indicating properties of trace.
+ * @since 0.16
+ */
+ public abstract byte getTraceOption();
+
+ /**
+ * Creates new {@link ServerStats} from specified parameters.
+ *
+ * @param lbLatencyNs Represents request processing latency observed on Load Balancer. It is
+ * measured in nanoseconds. Must not be less than 0. Value of 0 represents that the latency is
+ * not measured.
+ * @param serviceLatencyNs Represents request processing latency observed on Server. It is
+ * measured in nanoseconds. Must not be less than 0. Value of 0 represents that the latency is
+ * not measured.
+ * @param traceOption Represents set of bits to indicate properties of trace. Currently it used
+ * only the least signification bit to represent sampling of the request on the server side.
+ * Other bits are ignored.
+ * @return new {@code ServerStats} with specified fields.
+ * @throws IllegalArgumentException if the arguments are out of range.
+ * @since 0.16
+ */
+ public static ServerStats create(long lbLatencyNs, long serviceLatencyNs, byte traceOption) {
+
+ if (lbLatencyNs < 0) {
+ throw new IllegalArgumentException("'getLbLatencyNs' is less than zero: " + lbLatencyNs);
+ }
+
+ if (serviceLatencyNs < 0) {
+ throw new IllegalArgumentException(
+ "'getServiceLatencyNs' is less than zero: " + serviceLatencyNs);
+ }
+
+ return new AutoValue_ServerStats(lbLatencyNs, serviceLatencyNs, traceOption);
+ }
+}
diff --git a/api/src/main/java/io/opencensus/common/ServerStatsDeserializationException.java b/api/src/main/java/io/opencensus/common/ServerStatsDeserializationException.java
new file mode 100644
index 00000000..2332733c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ServerStatsDeserializationException.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018, 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.common;
+
+/**
+ * Exception thrown when a {@link ServerStats} cannot be parsed.
+ *
+ * @since 0.16
+ */
+public final class ServerStatsDeserializationException extends Exception {
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * Constructs a new {@code ServerStatsDeserializationException} with the given message.
+ *
+ * @param message a message describing the error.
+ * @since 0.16
+ */
+ public ServerStatsDeserializationException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code ServerStatsDeserializationException} with the given message and cause.
+ *
+ * @param message a message describing the error.
+ * @param cause the cause of the error.
+ * @since 0.16
+ */
+ public ServerStatsDeserializationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/api/src/main/java/io/opencensus/common/ServerStatsEncoding.java b/api/src/main/java/io/opencensus/common/ServerStatsEncoding.java
new file mode 100644
index 00000000..024a93f8
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ServerStatsEncoding.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2018, 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.common;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A service class to encode/decode {@link ServerStats} as defined by the spec.
+ *
+ * <p>See <a
+ * href="https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/CensusServerStatsEncoding.md">opencensus-server-stats-specs</a>
+ * for encoding {@code ServerStats}
+ *
+ * <p>Use {@code ServerStatsEncoding.toBytes(ServerStats stats)} to encode.
+ *
+ * <p>Use {@code ServerStatsEncoding.parseBytes(byte[] serialized)} to decode.
+ *
+ * @since 0.16
+ */
+public final class ServerStatsEncoding {
+
+ private ServerStatsEncoding() {}
+
+ /**
+ * The current encoding version. The value is {@value #CURRENT_VERSION}
+ *
+ * @since 0.16
+ */
+ public static final byte CURRENT_VERSION = (byte) 0;
+
+ /**
+ * Encodes the {@link ServerStats} as per the Opencensus Summary Span specification.
+ *
+ * @param stats {@code ServerStats} to encode.
+ * @return encoded byte array.
+ * @since 0.16
+ */
+ public static byte[] toBytes(ServerStats stats) {
+ // Should this be optimized to not include invalid values?
+
+ ByteBuffer bb = ByteBuffer.allocate(ServerStatsFieldEnums.getTotalSize() + 1);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+
+ // put version
+ bb.put(CURRENT_VERSION);
+
+ bb.put((byte) ServerStatsFieldEnums.Id.SERVER_STATS_LB_LATENCY_ID.value());
+ bb.putLong(stats.getLbLatencyNs());
+
+ bb.put((byte) ServerStatsFieldEnums.Id.SERVER_STATS_SERVICE_LATENCY_ID.value());
+ bb.putLong(stats.getServiceLatencyNs());
+
+ bb.put((byte) ServerStatsFieldEnums.Id.SERVER_STATS_TRACE_OPTION_ID.value());
+ bb.put(stats.getTraceOption());
+ return bb.array();
+ }
+
+ /**
+ * Decodes serialized byte array to create {@link ServerStats} as per Opencensus Summary Span
+ * specification.
+ *
+ * @param serialized encoded {@code ServerStats} in byte array.
+ * @return decoded {@code ServerStats}. null if decoding fails.
+ * @since 0.16
+ */
+ public static ServerStats parseBytes(byte[] serialized)
+ throws ServerStatsDeserializationException {
+ final ByteBuffer bb = ByteBuffer.wrap(serialized);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ long serviceLatencyNs = 0L;
+ long lbLatencyNs = 0L;
+ byte traceOption = (byte) 0;
+
+ // Check the version first.
+ if (!bb.hasRemaining()) {
+ throw new ServerStatsDeserializationException("Serialized ServerStats buffer is empty");
+ }
+ byte version = bb.get();
+
+ if (version > CURRENT_VERSION || version < 0) {
+ throw new ServerStatsDeserializationException("Invalid ServerStats version: " + version);
+ }
+
+ while (bb.hasRemaining()) {
+ ServerStatsFieldEnums.Id id = ServerStatsFieldEnums.Id.valueOf((int) bb.get() & 0xFF);
+ if (id == null) {
+ // Skip remaining;
+ bb.position(bb.limit());
+ } else {
+ switch (id) {
+ case SERVER_STATS_LB_LATENCY_ID:
+ lbLatencyNs = bb.getLong();
+ break;
+ case SERVER_STATS_SERVICE_LATENCY_ID:
+ serviceLatencyNs = bb.getLong();
+ break;
+ case SERVER_STATS_TRACE_OPTION_ID:
+ traceOption = bb.get();
+ break;
+ }
+ }
+ }
+ try {
+ return ServerStats.create(lbLatencyNs, serviceLatencyNs, traceOption);
+ } catch (IllegalArgumentException e) {
+ throw new ServerStatsDeserializationException(
+ "Serialized ServiceStats contains invalid values: " + e.getMessage());
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/common/ServerStatsFieldEnums.java b/api/src/main/java/io/opencensus/common/ServerStatsFieldEnums.java
new file mode 100644
index 00000000..ff3cfda9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ServerStatsFieldEnums.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2018, 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.common;
+
+import java.util.TreeMap;
+import javax.annotation.Nullable;
+
+/**
+ * A Enum representation for Ids and Size for attributes of {@code ServerStats}.
+ *
+ * <p>See <a
+ * href="https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/CensusServerStatsEncoding.md">opencensus-server-stats-specs</a>
+ * for the field ids and their length defined for Server Stats
+ *
+ * @since 0.16
+ */
+public final class ServerStatsFieldEnums {
+
+ /**
+ * Available Ids for {@code ServerStats} attributes.
+ *
+ * @since 0.16
+ */
+ public enum Id {
+ /**
+ * Id for Latency observed at Load Balancer.
+ *
+ * @since 0.16
+ */
+ SERVER_STATS_LB_LATENCY_ID(0),
+ /**
+ * Id for Latency observed at Server.
+ *
+ * @since 0.16
+ */
+ SERVER_STATS_SERVICE_LATENCY_ID(1),
+ /**
+ * Id for Trace options.
+ *
+ * @since 0.16
+ */
+ SERVER_STATS_TRACE_OPTION_ID(2);
+
+ private final int value;
+
+ private Id(int value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the numerical value of the {@link Id}.
+ *
+ * @return the numerical value of the {@code Id}.
+ * @since 0.16
+ */
+ public int value() {
+ return value;
+ }
+
+ private static final TreeMap<Integer, Id> map = new TreeMap<Integer, Id>();
+
+ static {
+ for (Id id : Id.values()) {
+ map.put(id.value, id);
+ }
+ }
+
+ /**
+ * Returns the {@link Id} representing the value value of the id.
+ *
+ * @param value integer value for which {@code Id} is being requested.
+ * @return the numerical value of the id. null if the id is not valid
+ * @since 0.16
+ */
+ @Nullable
+ public static Id valueOf(int value) {
+ return map.get(value);
+ }
+ }
+
+ /**
+ * Size for each attributes in {@code ServerStats}.
+ *
+ * @since 0.16
+ */
+ public enum Size {
+ /**
+ * Number of bytes used to represent latency observed at Load Balancer.
+ *
+ * @since 0.16
+ */
+ SERVER_STATS_LB_LATENCY_SIZE(8),
+ /**
+ * Number of bytes used to represent latency observed at Server.
+ *
+ * @since 0.16
+ */
+ SERVER_STATS_SERVICE_LATENCY_SIZE(8),
+ /**
+ * Number of bytes used to represent Trace option.
+ *
+ * @since 0.16
+ */
+ SERVER_STATS_TRACE_OPTION_SIZE(1);
+
+ private final int value;
+
+ private Size(int value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the numerical value of the {@link Size}.
+ *
+ * @return the numerical value of the {@code Size}.
+ * @since 0.16
+ */
+ public int value() {
+ return value;
+ }
+ }
+
+ private static final int TOTALSIZE = computeTotalSize();
+
+ private ServerStatsFieldEnums() {}
+
+ private static int computeTotalSize() {
+ int sum = 0;
+ for (Size sizeValue : Size.values()) {
+ sum += sizeValue.value();
+ sum += 1; // For Id
+ }
+ return sum;
+ }
+
+ /**
+ * Returns the total size required to encode the {@code ServerStats}.
+ *
+ * @return the total size required to encode all fields in {@code ServerStats}.
+ * @since 0.16
+ */
+ public static int getTotalSize() {
+ return TOTALSIZE;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/common/TimeUtils.java b/api/src/main/java/io/opencensus/common/TimeUtils.java
new file mode 100644
index 00000000..db119e2e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/TimeUtils.java
@@ -0,0 +1,59 @@
+/*
+ * 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.common;
+
+import java.math.BigInteger;
+
+/** Util class for {@link Timestamp} and {@link Duration}. */
+final class TimeUtils {
+ static final long MAX_SECONDS = 315576000000L;
+ static final int MAX_NANOS = 999999999;
+ static final long MILLIS_PER_SECOND = 1000L;
+ static final long NANOS_PER_MILLI = 1000 * 1000;
+ static final long NANOS_PER_SECOND = NANOS_PER_MILLI * MILLIS_PER_SECOND;
+
+ private TimeUtils() {}
+
+ /**
+ * Compares two longs. This functionality is provided by {@code Long.compare(long, long)} in Java
+ * 7.
+ */
+ static int compareLongs(long x, long y) {
+ if (x < y) {
+ return -1;
+ } else if (x == y) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+
+ private static final BigInteger MAX_LONG_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
+ private static final BigInteger MIN_LONG_VALUE = BigInteger.valueOf(Long.MIN_VALUE);
+
+ /**
+ * Adds two longs and throws an {@link ArithmeticException} if the result overflows. This
+ * functionality is provided by {@code Math.addExact(long, long)} in Java 8.
+ */
+ static long checkedAdd(long x, long y) {
+ BigInteger sum = BigInteger.valueOf(x).add(BigInteger.valueOf(y));
+ if (sum.compareTo(MAX_LONG_VALUE) > 0 || sum.compareTo(MIN_LONG_VALUE) < 0) {
+ throw new ArithmeticException("Long sum overflow: x=" + x + ", y=" + y);
+ }
+ return x + y;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/common/Timestamp.java b/api/src/main/java/io/opencensus/common/Timestamp.java
new file mode 100644
index 00000000..d17b3fd8
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/Timestamp.java
@@ -0,0 +1,200 @@
+/*
+ * 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.common;
+
+import static io.opencensus.common.TimeUtils.MAX_NANOS;
+import static io.opencensus.common.TimeUtils.MAX_SECONDS;
+import static io.opencensus.common.TimeUtils.MILLIS_PER_SECOND;
+import static io.opencensus.common.TimeUtils.NANOS_PER_MILLI;
+import static io.opencensus.common.TimeUtils.NANOS_PER_SECOND;
+
+import com.google.auto.value.AutoValue;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A representation of an instant in time. The instant is the number of nanoseconds after the number
+ * of seconds since the Unix Epoch.
+ *
+ * <p>Use {@code Tracing.getClock().now()} to get the current timestamp since epoch
+ * (1970-01-01T00:00:00Z).
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class Timestamp implements Comparable<Timestamp> {
+
+ Timestamp() {}
+
+ /**
+ * Creates a new timestamp from given seconds and nanoseconds.
+ *
+ * @param seconds Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must be
+ * from from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.
+ * @param nanos Non-negative fractions of a second at nanosecond resolution. Negative second
+ * values with fractions must still have non-negative nanos values that count forward in time.
+ * Must be from 0 to 999,999,999 inclusive.
+ * @return new {@code Timestamp} with specified fields.
+ * @throws IllegalArgumentException if the arguments are out of range.
+ * @since 0.5
+ */
+ public static Timestamp create(long seconds, int nanos) {
+ if (seconds < -MAX_SECONDS) {
+ throw new IllegalArgumentException(
+ "'seconds' is less than minimum (" + -MAX_SECONDS + "): " + seconds);
+ }
+ if (seconds > MAX_SECONDS) {
+ throw new IllegalArgumentException(
+ "'seconds' is greater than maximum (" + MAX_SECONDS + "): " + seconds);
+ }
+ if (nanos < 0) {
+ throw new IllegalArgumentException("'nanos' is less than zero: " + nanos);
+ }
+ if (nanos > MAX_NANOS) {
+ throw new IllegalArgumentException(
+ "'nanos' is greater than maximum (" + MAX_NANOS + "): " + nanos);
+ }
+ return new AutoValue_Timestamp(seconds, nanos);
+ }
+
+ /**
+ * Creates a new timestamp from the given milliseconds.
+ *
+ * @param epochMilli the timestamp represented in milliseconds since epoch.
+ * @return new {@code Timestamp} with specified fields.
+ * @throws IllegalArgumentException if the number of milliseconds is out of the range that can be
+ * represented by {@code Timestamp}.
+ * @since 0.5
+ */
+ public static Timestamp fromMillis(long epochMilli) {
+ long secs = floorDiv(epochMilli, MILLIS_PER_SECOND);
+ int mos = (int) floorMod(epochMilli, MILLIS_PER_SECOND);
+ return create(secs, (int) (mos * NANOS_PER_MILLI)); // Safe int * NANOS_PER_MILLI
+ }
+
+ /**
+ * Returns the number of seconds since the Unix Epoch represented by this timestamp.
+ *
+ * @return the number of seconds since the Unix Epoch.
+ * @since 0.5
+ */
+ public abstract long getSeconds();
+
+ /**
+ * Returns the number of nanoseconds after the number of seconds since the Unix Epoch represented
+ * by this timestamp.
+ *
+ * @return the number of nanoseconds after the number of seconds since the Unix Epoch.
+ * @since 0.5
+ */
+ public abstract int getNanos();
+
+ /**
+ * Returns a {@code Timestamp} calculated as this {@code Timestamp} plus some number of
+ * nanoseconds.
+ *
+ * @param nanosToAdd the nanos to add, positive or negative.
+ * @return the calculated {@code Timestamp}. For invalid inputs, a {@code Timestamp} of zero is
+ * returned.
+ * @throws ArithmeticException if numeric overflow occurs.
+ * @since 0.5
+ */
+ public Timestamp addNanos(long nanosToAdd) {
+ return plus(0, nanosToAdd);
+ }
+
+ /**
+ * Returns a {@code Timestamp} calculated as this {@code Timestamp} plus some {@code Duration}.
+ *
+ * @param duration the {@code Duration} to add.
+ * @return a {@code Timestamp} with the specified {@code Duration} added.
+ * @since 0.5
+ */
+ public Timestamp addDuration(Duration duration) {
+ return plus(duration.getSeconds(), duration.getNanos());
+ }
+
+ /**
+ * Returns a {@link Duration} calculated as: {@code this - timestamp}.
+ *
+ * @param timestamp the {@code Timestamp} to subtract.
+ * @return the calculated {@code Duration}. For invalid inputs, a {@code Duration} of zero is
+ * returned.
+ * @since 0.5
+ */
+ public Duration subtractTimestamp(Timestamp timestamp) {
+ long durationSeconds = getSeconds() - timestamp.getSeconds();
+ int durationNanos = getNanos() - timestamp.getNanos();
+ if (durationSeconds < 0 && durationNanos > 0) {
+ durationSeconds += 1;
+ durationNanos = (int) (durationNanos - NANOS_PER_SECOND);
+ } else if (durationSeconds > 0 && durationNanos < 0) {
+ durationSeconds -= 1;
+ durationNanos = (int) (durationNanos + NANOS_PER_SECOND);
+ }
+ return Duration.create(durationSeconds, durationNanos);
+ }
+
+ /**
+ * Compares this {@code Timestamp} to the specified {@code Timestamp}.
+ *
+ * @param otherTimestamp the other {@code Timestamp} to compare to, not {@code null}.
+ * @return the comparator value: zero if equal, negative if this timestamp happens before
+ * otherTimestamp, positive if after.
+ * @throws NullPointerException if otherTimestamp is {@code null}.
+ */
+ @Override
+ public int compareTo(Timestamp otherTimestamp) {
+ int cmp = TimeUtils.compareLongs(getSeconds(), otherTimestamp.getSeconds());
+ if (cmp != 0) {
+ return cmp;
+ }
+ return TimeUtils.compareLongs(getNanos(), otherTimestamp.getNanos());
+ }
+
+ // Returns a Timestamp with the specified duration added.
+ private Timestamp plus(long secondsToAdd, long nanosToAdd) {
+ if ((secondsToAdd | nanosToAdd) == 0) {
+ return this;
+ }
+ long epochSec = TimeUtils.checkedAdd(getSeconds(), secondsToAdd);
+ epochSec = TimeUtils.checkedAdd(epochSec, nanosToAdd / NANOS_PER_SECOND);
+ nanosToAdd = nanosToAdd % NANOS_PER_SECOND;
+ long nanoAdjustment = getNanos() + nanosToAdd; // safe int + NANOS_PER_SECOND
+ return ofEpochSecond(epochSec, nanoAdjustment);
+ }
+
+ // Returns a Timestamp calculated using seconds from the epoch and nanosecond fraction of
+ // second (arbitrary number of nanoseconds).
+ private static Timestamp ofEpochSecond(long epochSecond, long nanoAdjustment) {
+ long secs = TimeUtils.checkedAdd(epochSecond, floorDiv(nanoAdjustment, NANOS_PER_SECOND));
+ int nos = (int) floorMod(nanoAdjustment, NANOS_PER_SECOND);
+ return create(secs, nos);
+ }
+
+ // Returns the result of dividing x by y rounded using floor.
+ private static long floorDiv(long x, long y) {
+ return BigDecimal.valueOf(x).divide(BigDecimal.valueOf(y), 0, RoundingMode.FLOOR).longValue();
+ }
+
+ // Returns the floor modulus "x - (floorDiv(x, y) * y)"
+ private static long floorMod(long x, long y) {
+ return x - floorDiv(x, y) * y;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/common/ToDoubleFunction.java b/api/src/main/java/io/opencensus/common/ToDoubleFunction.java
new file mode 100644
index 00000000..6ace2f7c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ToDoubleFunction.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.common;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Represents a function that produces a double-valued result. See {@link
+ * io.opencensus.metrics.MetricRegistry} for an example of its use.
+ *
+ * <p>Note: This class is based on the java.util.ToDoubleFunction class added in Java 1.8. We cannot
+ * use the Function from Java 1.8 because this library is Java 1.6 compatible.
+ *
+ * @since 0.16
+ */
+public interface ToDoubleFunction</*@Nullable*/ T> {
+
+ /**
+ * Applies this function to the given argument.
+ *
+ * @param value the function argument.
+ * @return the function result.
+ */
+ double applyAsDouble(/*@Nullable*/ T value);
+}
diff --git a/api/src/main/java/io/opencensus/common/ToLongFunction.java b/api/src/main/java/io/opencensus/common/ToLongFunction.java
new file mode 100644
index 00000000..cd2b68ed
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/ToLongFunction.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018, 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.common;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Represents a function that produces a long-valued result. See {@link
+ * io.opencensus.metrics.MetricRegistry} for an example of its use.
+ *
+ * <p>Note: This class is based on the java.util.ToLongFunction class added in Java 1.8. We cannot
+ * use the Function from Java 1.8 because this library is Java 1.6 compatible.
+ *
+ * @since 0.16
+ */
+public interface ToLongFunction</*@Nullable*/ T> {
+ /**
+ * Applies this function to the given argument.
+ *
+ * @param value the function argument.
+ * @return the function result.
+ */
+ long applyAsLong(/*@Nullable*/ T value);
+}
diff --git a/api/src/main/java/io/opencensus/common/package-info.java b/api/src/main/java/io/opencensus/common/package-info.java
new file mode 100644
index 00000000..1ebfd7cf
--- /dev/null
+++ b/api/src/main/java/io/opencensus/common/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/** Common API between different packages in this artifact. */
+package io.opencensus.common;
diff --git a/api/src/main/java/io/opencensus/internal/DefaultVisibilityForTesting.java b/api/src/main/java/io/opencensus/internal/DefaultVisibilityForTesting.java
new file mode 100644
index 00000000..e90a6573
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/DefaultVisibilityForTesting.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that an element is package-private instead of private only for the purpose of testing.
+ * This annotation is only meant to be used as documentation in the source code.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({
+ ElementType.ANNOTATION_TYPE,
+ ElementType.CONSTRUCTOR,
+ ElementType.FIELD,
+ ElementType.METHOD,
+ ElementType.PACKAGE,
+ ElementType.TYPE
+})
+public @interface DefaultVisibilityForTesting {}
diff --git a/api/src/main/java/io/opencensus/internal/NoopScope.java b/api/src/main/java/io/opencensus/internal/NoopScope.java
new file mode 100644
index 00000000..f4a8da07
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/NoopScope.java
@@ -0,0 +1,38 @@
+/*
+ * 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.internal;
+
+import io.opencensus.common.Scope;
+
+/** A {@link Scope} that does nothing when it is created or closed. */
+public final class NoopScope implements Scope {
+ private static final Scope INSTANCE = new NoopScope();
+
+ private NoopScope() {}
+
+ /**
+ * Returns a {@code NoopScope}.
+ *
+ * @return a {@code NoopScope}.
+ */
+ public static Scope getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void close() {}
+}
diff --git a/api/src/main/java/io/opencensus/internal/Provider.java b/api/src/main/java/io/opencensus/internal/Provider.java
new file mode 100644
index 00000000..8cfb7294
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/Provider.java
@@ -0,0 +1,49 @@
+/*
+ * 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.internal;
+
+import java.util.ServiceConfigurationError;
+
+/**
+ * OpenCensus service provider mechanism.
+ *
+ * <pre>{@code
+ * // Initialize a variable using reflection.
+ * foo = Provider.createInstance(
+ * Class.forName("FooImpl", true, classLoader), Foo.class);
+ * }</pre>
+ */
+public final class Provider {
+ private Provider() {}
+
+ /**
+ * Tries to create an instance of the given rawClass as a subclass of the given superclass.
+ *
+ * @param rawClass The class that is initialized.
+ * @param superclass The initialized class must be a subclass of this.
+ * @return an instance of the class given rawClass which is a subclass of the given superclass.
+ * @throws ServiceConfigurationError if any error happens.
+ */
+ public static <T> T createInstance(Class<?> rawClass, Class<T> superclass) {
+ try {
+ return rawClass.asSubclass(superclass).getConstructor().newInstance();
+ } catch (Exception e) {
+ throw new ServiceConfigurationError(
+ "Provider " + rawClass.getName() + " could not be instantiated.", e);
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/internal/StringUtils.java b/api/src/main/java/io/opencensus/internal/StringUtils.java
new file mode 100644
index 00000000..717e333c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/StringUtils.java
@@ -0,0 +1,42 @@
+/*
+ * 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.internal;
+
+/** Internal utility methods for working with tag keys, tag values, and metric names. */
+public final class StringUtils {
+
+ /**
+ * Determines whether the {@code String} contains only printable characters.
+ *
+ * @param str the {@code String} to be validated.
+ * @return whether the {@code String} contains only printable characters.
+ */
+ public static boolean isPrintableString(String str) {
+ for (int i = 0; i < str.length(); i++) {
+ if (!isPrintableChar(str.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean isPrintableChar(char ch) {
+ return ch >= ' ' && ch <= '~';
+ }
+
+ private StringUtils() {}
+}
diff --git a/api/src/main/java/io/opencensus/internal/Utils.java b/api/src/main/java/io/opencensus/internal/Utils.java
new file mode 100644
index 00000000..df5c9840
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/Utils.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import java.util.List;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.NonNull;
+*/
+
+/** General internal utility methods. */
+public final class Utils {
+
+ private Utils() {}
+
+ /**
+ * Throws an {@link IllegalArgumentException} if the argument is false. This method is similar to
+ * {@code Preconditions.checkArgument(boolean, Object)} from Guava.
+ *
+ * @param isValid whether the argument check passed.
+ * @param errorMessage the message to use for the exception. Will be converted to a string using
+ * {@link String#valueOf(Object)}.
+ */
+ public static void checkArgument(
+ boolean isValid, @javax.annotation.Nullable Object errorMessage) {
+ if (!isValid) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Throws an {@link IllegalArgumentException} if the argument is false. This method is similar to
+ * {@code Preconditions.checkArgument(boolean, Object)} from Guava.
+ *
+ * @param expression a boolean expression
+ * @param errorMessageTemplate a template for the exception message should the check fail. The
+ * message is formed by replacing each {@code %s} placeholder in the template with an
+ * argument. These are matched by position - the first {@code %s} gets {@code
+ * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message in
+ * square braces. Unmatched placeholders will be left as-is.
+ * @param errorMessageArgs the arguments to be substituted into the message template. Arguments
+ * are converted to strings using {@link String#valueOf(Object)}.
+ * @throws IllegalArgumentException if {@code expression} is false
+ * @throws NullPointerException if the check fails and either {@code errorMessageTemplate} or
+ * {@code errorMessageArgs} is null (don't let this happen)
+ */
+ public static void checkArgument(
+ boolean expression,
+ String errorMessageTemplate,
+ @javax.annotation.Nullable Object... errorMessageArgs) {
+ if (!expression) {
+ throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs));
+ }
+ }
+
+ /**
+ * Throws an {@link IllegalStateException} if the argument is false. This method is similar to
+ * {@code Preconditions.checkState(boolean, Object)} from Guava.
+ *
+ * @param isValid whether the state check passed.
+ * @param errorMessage the message to use for the exception. Will be converted to a string using
+ * {@link String#valueOf(Object)}.
+ */
+ public static void checkState(boolean isValid, @javax.annotation.Nullable Object errorMessage) {
+ if (!isValid) {
+ throw new IllegalStateException(String.valueOf(errorMessage));
+ }
+ }
+
+ /**
+ * Validates an index in an array or other container. This method throws an {@link
+ * IllegalArgumentException} if the size is negative and throws an {@link
+ * IndexOutOfBoundsException} if the index is negative or greater than or equal to the size. This
+ * method is similar to {@code Preconditions.checkElementIndex(int, int)} from Guava.
+ *
+ * @param index the index to validate.
+ * @param size the size of the array or container.
+ */
+ public static void checkIndex(int index, int size) {
+ if (size < 0) {
+ throw new IllegalArgumentException("Negative size: " + size);
+ }
+ if (index < 0 || index >= size) {
+ throw new IndexOutOfBoundsException("Index out of bounds: size=" + size + ", index=" + index);
+ }
+ }
+
+ /**
+ * Throws a {@link NullPointerException} if the argument is null. This method is similar to {@code
+ * Preconditions.checkNotNull(Object, Object)} from Guava.
+ *
+ * @param arg the argument to check for null.
+ * @param errorMessage the message to use for the exception. Will be converted to a string using
+ * {@link String#valueOf(Object)}.
+ * @return the argument, if it passes the null check.
+ */
+ public static <T /*>>> extends @NonNull Object*/> T checkNotNull(
+ T arg, @javax.annotation.Nullable Object errorMessage) {
+ if (arg == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return arg;
+ }
+
+ /**
+ * Throws a {@link NullPointerException} if any of the list elements is null.
+ *
+ * @param list the argument list to check for null.
+ * @param errorMessage the message to use for the exception. Will be converted to a string using
+ * {@link String#valueOf(Object)}.
+ */
+ public static <T /*>>> extends @NonNull Object*/> void checkListElementNotNull(
+ List<T> list, @javax.annotation.Nullable Object errorMessage) {
+ for (T element : list) {
+ if (element == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ }
+ }
+
+ /**
+ * Compares two Objects for equality. This functionality is provided by {@code
+ * Objects.equal(Object, Object)} in Java 7.
+ */
+ public static boolean equalsObjects(
+ @javax.annotation.Nullable Object x, @javax.annotation.Nullable Object y) {
+ return x == null ? y == null : x.equals(y);
+ }
+
+ /**
+ * Substitutes each {@code %s} in {@code template} with an argument. These are matched by
+ * position: the first {@code %s} gets {@code args[0]}, etc. If there are more arguments than
+ * placeholders, the unmatched arguments will be appended to the end of the formatted message in
+ * square braces.
+ *
+ * <p>Copied from {@code Preconditions.format(String, Object...)} from Guava
+ *
+ * @param template a non-null string containing 0 or more {@code %s} placeholders.
+ * @param args the arguments to be substituted into the message template. Arguments are converted
+ * to strings using {@link String#valueOf(Object)}. Arguments can be null.
+ */
+ // Note that this is somewhat-improperly used from Verify.java as well.
+ private static String format(String template, @javax.annotation.Nullable Object... args) {
+ // If no arguments return the template.
+ if (args == null) {
+ return template;
+ }
+
+ // start substituting the arguments into the '%s' placeholders
+ StringBuilder builder = new StringBuilder(template.length() + 16 * args.length);
+ int templateStart = 0;
+ int i = 0;
+ while (i < args.length) {
+ int placeholderStart = template.indexOf("%s", templateStart);
+ if (placeholderStart == -1) {
+ break;
+ }
+ builder.append(template, templateStart, placeholderStart);
+ builder.append(args[i++]);
+ templateStart = placeholderStart + 2;
+ }
+ builder.append(template, templateStart, template.length());
+
+ // if we run out of placeholders, append the extra args in square braces
+ if (i < args.length) {
+ builder.append(" [");
+ builder.append(args[i++]);
+ while (i < args.length) {
+ builder.append(", ");
+ builder.append(args[i++]);
+ }
+ builder.append(']');
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/api/src/main/java/io/opencensus/internal/ZeroTimeClock.java b/api/src/main/java/io/opencensus/internal/ZeroTimeClock.java
new file mode 100644
index 00000000..fda13e9e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/ZeroTimeClock.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.internal;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.Immutable;
+
+/** A {@link Clock} that always returns 0. */
+@Immutable
+public final class ZeroTimeClock extends Clock {
+ private static final ZeroTimeClock INSTANCE = new ZeroTimeClock();
+ private static final Timestamp ZERO_TIMESTAMP = Timestamp.create(0, 0);
+
+ private ZeroTimeClock() {}
+
+ /**
+ * Returns a {@code ZeroTimeClock}.
+ *
+ * @return a {@code ZeroTimeClock}.
+ */
+ public static ZeroTimeClock getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public Timestamp now() {
+ return ZERO_TIMESTAMP;
+ }
+
+ @Override
+ public long nowNanos() {
+ return 0;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/internal/package-info.java b/api/src/main/java/io/opencensus/internal/package-info.java
new file mode 100644
index 00000000..5dd35b23
--- /dev/null
+++ b/api/src/main/java/io/opencensus/internal/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * Interfaces and implementations that are internal to OpenCensus.
+ *
+ * <p>All the content under this package and its subpackages are considered annotated with {@link
+ * io.opencensus.common.Internal}.
+ */
+@io.opencensus.common.Internal
+package io.opencensus.internal;
diff --git a/api/src/main/java/io/opencensus/metrics/DerivedDoubleGauge.java b/api/src/main/java/io/opencensus/metrics/DerivedDoubleGauge.java
new file mode 100644
index 00000000..3aaca153
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/DerivedDoubleGauge.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.internal.Utils;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import javax.annotation.concurrent.ThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Derived Double Gauge metric, to report instantaneous measurement of a double value. Gauges can go
+ * both up and down. The gauges values can be negative.
+ *
+ * <p>Example: Create a Gauge with an object and a callback function.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ * private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ * List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ * List<LabelValue> labelValues = Arrays.asList(LabelValue.create("Inbound"));
+ *
+ * DerivedDoubleGauge gauge = metricRegistry.addDerivedDoubleGauge(
+ * "queue_size", "Pending jobs in a queue", "1", labelKeys);
+ *
+ * QueueManager queueManager = new QueueManager();
+ * gauge.createTimeSeries(labelValues, queueManager,
+ * new ToDoubleFunction<QueueManager>() {
+ * {@literal @}Override
+ * public double applyAsDouble(QueueManager queue) {
+ * return queue.size();
+ * }
+ * });
+ *
+ * void doWork() {
+ * // Your code here.
+ * }
+ * }
+ *
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public abstract class DerivedDoubleGauge {
+ /**
+ * Creates a {@code TimeSeries}. The value of a single point in the TimeSeries is observed from a
+ * callback function. This function is invoked whenever metrics are collected, meaning the
+ * reported value is up-to-date. It keeps a {@link WeakReference} to the object and it is the
+ * user's responsibility to manage the lifetime of the object.
+ *
+ * @param labelValues the list of label values.
+ * @param obj the state object from which the function derives a measurement.
+ * @param function the function to be called.
+ * @param <T> the type of the object upon which the function derives a measurement.
+ * @throws NullPointerException if {@code labelValues} is null OR any element of {@code
+ * labelValues} is null OR {@code function} is null.
+ * @throws IllegalArgumentException if different time series with the same labels already exists
+ * OR if number of {@code labelValues}s are not equal to the label keys.
+ * @since 0.17
+ */
+ public abstract <T> void createTimeSeries(
+ List<LabelValue> labelValues,
+ /*@Nullable*/ T obj,
+ ToDoubleFunction</*@Nullable*/ T> function);
+
+ /**
+ * Removes the {@code TimeSeries} from the gauge metric, if it is present.
+ *
+ * @param labelValues the list of label values.
+ * @throws NullPointerException if {@code labelValues} is null.
+ * @since 0.17
+ */
+ public abstract void removeTimeSeries(List<LabelValue> labelValues);
+
+ /**
+ * Removes all {@code TimeSeries} from the gauge metric.
+ *
+ * @since 0.17
+ */
+ public abstract void clear();
+
+ /**
+ * Returns the no-op implementation of the {@code DerivedDoubleGauge}.
+ *
+ * @return the no-op implementation of the {@code DerivedDoubleGauge}.
+ * @since 0.17
+ */
+ static DerivedDoubleGauge newNoopDerivedDoubleGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ return NoopDerivedDoubleGauge.create(name, description, unit, labelKeys);
+ }
+
+ /** No-op implementations of DerivedDoubleGauge class. */
+ private static final class NoopDerivedDoubleGauge extends DerivedDoubleGauge {
+ private final int labelKeysSize;
+
+ static NoopDerivedDoubleGauge create(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ return new NoopDerivedDoubleGauge(name, description, unit, labelKeys);
+ }
+
+ /** Creates a new {@code NoopDerivedDoubleGauge}. */
+ NoopDerivedDoubleGauge(String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkNotNull(name, "name");
+ Utils.checkNotNull(description, "description");
+ Utils.checkNotNull(unit, "unit");
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ labelKeysSize = labelKeys.size();
+ }
+
+ @Override
+ public <T> void createTimeSeries(
+ List<LabelValue> labelValues,
+ /*@Nullable*/ T obj,
+ ToDoubleFunction</*@Nullable*/ T> function) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+ Utils.checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+ Utils.checkNotNull(function, "function");
+ }
+
+ @Override
+ public void removeTimeSeries(List<LabelValue> labelValues) {
+ Utils.checkNotNull(labelValues, "labelValues");
+ }
+
+ @Override
+ public void clear() {}
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/DerivedLongGauge.java b/api/src/main/java/io/opencensus/metrics/DerivedLongGauge.java
new file mode 100644
index 00000000..621873f9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/DerivedLongGauge.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.internal.Utils;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import javax.annotation.concurrent.ThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Derived Long Gauge metric, to report instantaneous measurement of an int64 value. Gauges can go
+ * both up and down. The gauges values can be negative.
+ *
+ * <p>Example: Create a Gauge with an object and a callback function.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ * private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ * List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ * List<LabelValue> labelValues = Arrays.asList(LabelValue.create("Inbound"));
+ *
+ * DerivedLongGauge gauge = metricRegistry.addDerivedLongGauge(
+ * "queue_size", "Pending jobs in a queue", "1", labelKeys);
+ *
+ * QueueManager queueManager = new QueueManager();
+ * gauge.createTimeSeries(labelValues, queueManager,
+ * new ToLongFunction<QueueManager>() {
+ * {@literal @}Override
+ * public long applyAsLong(QueueManager queue) {
+ * return queue.size();
+ * }
+ * });
+ *
+ * void doWork() {
+ * // Your code here.
+ * }
+ * }
+ *
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public abstract class DerivedLongGauge {
+ /**
+ * Creates a {@code TimeSeries}. The value of a single point in the TimeSeries is observed from a
+ * callback function. This function is invoked whenever metrics are collected, meaning the
+ * reported value is up-to-date. It keeps a {@link WeakReference} to the object and it is the
+ * user's responsibility to manage the lifetime of the object.
+ *
+ * @param labelValues the list of label values.
+ * @param obj the state object from which the function derives a measurement.
+ * @param function the function to be called.
+ * @param <T> the type of the object upon which the function derives a measurement.
+ * @throws NullPointerException if {@code labelValues} is null OR any element of {@code
+ * labelValues} is null OR {@code function} is null.
+ * @throws IllegalArgumentException if different time series with the same labels already exists
+ * OR if number of {@code labelValues}s are not equal to the label keys.
+ * @since 0.17
+ */
+ public abstract <T> void createTimeSeries(
+ List<LabelValue> labelValues, /*@Nullable*/ T obj, ToLongFunction</*@Nullable*/ T> function);
+
+ /**
+ * Removes the {@code TimeSeries} from the gauge metric, if it is present.
+ *
+ * @param labelValues the list of label values.
+ * @throws NullPointerException if {@code labelValues} is null.
+ * @since 0.17
+ */
+ public abstract void removeTimeSeries(List<LabelValue> labelValues);
+
+ /**
+ * Removes all {@code TimeSeries} from the gauge metric.
+ *
+ * @since 0.17
+ */
+ public abstract void clear();
+
+ /**
+ * Returns the no-op implementation of the {@code DerivedLongGauge}.
+ *
+ * @return the no-op implementation of the {@code DerivedLongGauge}.
+ * @since 0.17
+ */
+ static DerivedLongGauge newNoopDerivedLongGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ return NoopDerivedLongGauge.create(name, description, unit, labelKeys);
+ }
+
+ /** No-op implementations of DerivedLongGauge class. */
+ private static final class NoopDerivedLongGauge extends DerivedLongGauge {
+ private final int labelKeysSize;
+
+ static NoopDerivedLongGauge create(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ return new NoopDerivedLongGauge(name, description, unit, labelKeys);
+ }
+
+ /** Creates a new {@code NoopDerivedLongGauge}. */
+ NoopDerivedLongGauge(String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkNotNull(name, "name");
+ Utils.checkNotNull(description, "description");
+ Utils.checkNotNull(unit, "unit");
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ labelKeysSize = labelKeys.size();
+ }
+
+ @Override
+ public <T> void createTimeSeries(
+ List<LabelValue> labelValues,
+ /*@Nullable*/ T obj,
+ ToLongFunction</*@Nullable*/ T> function) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+ Utils.checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+ Utils.checkNotNull(function, "function");
+ }
+
+ @Override
+ public void removeTimeSeries(List<LabelValue> labelValues) {
+ Utils.checkNotNull(labelValues, "labelValues");
+ }
+
+ @Override
+ public void clear() {}
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/DoubleGauge.java b/api/src/main/java/io/opencensus/metrics/DoubleGauge.java
new file mode 100644
index 00000000..32759973
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/DoubleGauge.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.internal.Utils;
+import java.util.List;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Double Gauge metric, to report instantaneous measurement of a double value. Gauges can go both up
+ * and down. The gauges values can be negative.
+ *
+ * <p>Example 1: Create a Gauge with default labels.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ * private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ * List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ *
+ * DoubleGauge gauge = metricRegistry.addDoubleGauge("queue_size",
+ * "Pending jobs", "1", labelKeys);
+ *
+ * // It is recommended to keep a reference of a point for manual operations.
+ * DoublePoint defaultPoint = gauge.getDefaultTimeSeries();
+ *
+ * void doWork() {
+ * // Your code here.
+ * defaultPoint.add(10);
+ * }
+ *
+ * }
+ * }</pre>
+ *
+ * <p>Example 2: You can also use labels(keys and values) to track different types of metric.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ * private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ * List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ * List<LabelValue> labelValues = Arrays.asList(LabelValue.create("Inbound"));
+ *
+ * DoubleGauge gauge = metricRegistry.addDoubleGauge("queue_size",
+ * "Pending jobs", "1", labelKeys);
+ *
+ * // It is recommended to keep a reference of a point for manual operations.
+ * DoublePoint inboundPoint = gauge.getOrCreateTimeSeries(labelValues);
+ *
+ * void doSomeWork() {
+ * // Your code here.
+ * inboundPoint.set(15);
+ * }
+ *
+ * }
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public abstract class DoubleGauge {
+
+ /**
+ * Creates a {@code TimeSeries} and returns a {@code DoublePoint} if the specified {@code
+ * labelValues} is not already associated with this gauge, else returns an existing {@code
+ * DoublePoint}.
+ *
+ * <p>It is recommended to keep a reference to the DoublePoint instead of always calling this
+ * method for manual operations.
+ *
+ * @param labelValues the list of label values. The number of label values must be the same to
+ * that of the label keys passed to {@link MetricRegistry#addDoubleGauge}.
+ * @return a {@code DoublePoint} the value of single gauge.
+ * @throws NullPointerException if {@code labelValues} is null OR any element of {@code
+ * labelValues} is null.
+ * @throws IllegalArgumentException if number of {@code labelValues}s are not equal to the label
+ * keys.
+ * @since 0.17
+ */
+ public abstract DoublePoint getOrCreateTimeSeries(List<LabelValue> labelValues);
+
+ /**
+ * Returns a {@code DoublePoint} for a gauge with all labels not set, or default labels.
+ *
+ * @return a {@code DoublePoint} for a gauge with all labels not set, or default labels.
+ * @since 0.17
+ */
+ public abstract DoublePoint getDefaultTimeSeries();
+
+ /**
+ * Removes the {@code TimeSeries} from the gauge metric, if it is present. i.e. references to
+ * previous {@code DoublePoint} objects are invalid (not part of the metric).
+ *
+ * @param labelValues the list of label values.
+ * @throws NullPointerException if {@code labelValues} is null or any element of {@code
+ * labelValues} is null.
+ * @since 0.17
+ */
+ public abstract void removeTimeSeries(List<LabelValue> labelValues);
+
+ /**
+ * Removes all {@code TimeSeries} from the gauge metric. i.e. references to all previous {@code
+ * DoublePoint} objects are invalid (not part of the metric).
+ *
+ * @since 0.17
+ */
+ public abstract void clear();
+
+ /**
+ * Returns the no-op implementation of the {@code DoubleGauge}.
+ *
+ * @return the no-op implementation of the {@code DoubleGauge}.
+ * @since 0.17
+ */
+ static DoubleGauge newNoopDoubleGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ return NoopDoubleGauge.create(name, description, unit, labelKeys);
+ }
+
+ /**
+ * The value of a single point in the Gauge.TimeSeries.
+ *
+ * @since 0.17
+ */
+ public abstract static class DoublePoint {
+
+ /**
+ * Adds the given value to the current value. The values can be negative.
+ *
+ * @param amt the value to add
+ * @since 0.17
+ */
+ public abstract void add(double amt);
+
+ /**
+ * Sets the given value.
+ *
+ * @param val the new value.
+ * @since 0.17
+ */
+ public abstract void set(double val);
+ }
+
+ /** No-op implementations of DoubleGauge class. */
+ private static final class NoopDoubleGauge extends DoubleGauge {
+ private final int labelKeysSize;
+
+ static NoopDoubleGauge create(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ return new NoopDoubleGauge(name, description, unit, labelKeys);
+ }
+
+ /** Creates a new {@code NoopDoublePoint}. */
+ NoopDoubleGauge(String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkNotNull(name, "name");
+ Utils.checkNotNull(description, "description");
+ Utils.checkNotNull(unit, "unit");
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ labelKeysSize = labelKeys.size();
+ }
+
+ @Override
+ public NoopDoublePoint getOrCreateTimeSeries(List<LabelValue> labelValues) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+ Utils.checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+ return NoopDoublePoint.INSTANCE;
+ }
+
+ @Override
+ public NoopDoublePoint getDefaultTimeSeries() {
+ return NoopDoublePoint.INSTANCE;
+ }
+
+ @Override
+ public void removeTimeSeries(List<LabelValue> labelValues) {
+ Utils.checkNotNull(labelValues, "labelValues");
+ }
+
+ @Override
+ public void clear() {}
+
+ /** No-op implementations of DoublePoint class. */
+ private static final class NoopDoublePoint extends DoublePoint {
+ private static final NoopDoublePoint INSTANCE = new NoopDoublePoint();
+
+ private NoopDoublePoint() {}
+
+ @Override
+ public void add(double amt) {}
+
+ @Override
+ public void set(double val) {}
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/LabelKey.java b/api/src/main/java/io/opencensus/metrics/LabelKey.java
new file mode 100644
index 00000000..efc51e64
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/LabelKey.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The key of a {@code Label} associated with a {@code MetricDescriptor}.
+ *
+ * @since 0.15
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class LabelKey {
+
+ LabelKey() {}
+
+ /**
+ * Creates a {@link LabelKey}.
+ *
+ * @param key the key of a {@code Label}.
+ * @param description a human-readable description of what this label key represents.
+ * @return a {@code LabelKey}.
+ * @since 0.17
+ */
+ public static LabelKey create(String key, String description) {
+ return new AutoValue_LabelKey(key, description);
+ }
+
+ /**
+ * Returns the key of this {@link LabelKey}.
+ *
+ * @return the key.
+ * @since 0.17
+ */
+ public abstract String getKey();
+
+ /**
+ * Returns the description of this {@link LabelKey}.
+ *
+ * @return the description.
+ * @since 0.17
+ */
+ public abstract String getDescription();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/LabelValue.java b/api/src/main/java/io/opencensus/metrics/LabelValue.java
new file mode 100644
index 00000000..e5708655
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/LabelValue.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The value of a {@code Label} associated with a {@code TimeSeries}.
+ *
+ * @since 0.15
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class LabelValue {
+
+ LabelValue() {}
+
+ /**
+ * Creates a {@link LabelValue}.
+ *
+ * @param value the value of a {@code Label}. {@code null} value indicates an unset {@code
+ * LabelValue}.
+ * @return a {@code LabelValue}.
+ * @since 0.17
+ */
+ public static LabelValue create(@Nullable String value) {
+ return new AutoValue_LabelValue(value);
+ }
+
+ /**
+ * Returns the value of this {@link LabelValue}. Returns {@code null} if the value is unset and
+ * supposed to be ignored.
+ *
+ * @return the value.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract String getValue();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/LongGauge.java b/api/src/main/java/io/opencensus/metrics/LongGauge.java
new file mode 100644
index 00000000..1d4489c9
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/LongGauge.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.internal.Utils;
+import java.util.List;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Long Gauge metric, to report instantaneous measurement of an int64 value. Gauges can go both up
+ * and down. The gauges values can be negative.
+ *
+ * <p>Example 1: Create a Gauge with default labels.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ * private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ * List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ *
+ * LongGauge gauge = metricRegistry.addLongGauge("queue_size", "Pending jobs", "1", labelKeys);
+ *
+ * // It is recommended to keep a reference of a point for manual operations.
+ * LongPoint defaultPoint = gauge.getDefaultTimeSeries();
+ *
+ * void doWork() {
+ * // Your code here.
+ * defaultPoint.add(10);
+ * }
+ *
+ * }
+ * }</pre>
+ *
+ * <p>Example 2: You can also use labels(keys and values) to track different types of metric.
+ *
+ * <pre>{@code
+ * class YourClass {
+ *
+ * private static final MetricRegistry metricRegistry = Metrics.getMetricRegistry();
+ *
+ * List<LabelKey> labelKeys = Arrays.asList(LabelKey.create("Name", "desc"));
+ * List<LabelValue> labelValues = Arrays.asList(LabelValue.create("Inbound"));
+ *
+ * LongGauge gauge = metricRegistry.addLongGauge("queue_size", "Pending jobs", "1", labelKeys);
+ *
+ * // It is recommended to keep a reference of a point for manual operations.
+ * LongPoint inboundPoint = gauge.getOrCreateTimeSeries(labelValues);
+ *
+ * void doSomeWork() {
+ * // Your code here.
+ * inboundPoint.set(15);
+ * }
+ *
+ * }
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public abstract class LongGauge {
+
+ /**
+ * Creates a {@code TimeSeries} and returns a {@code LongPoint} if the specified {@code
+ * labelValues} is not already associated with this gauge, else returns an existing {@code
+ * LongPoint}.
+ *
+ * <p>It is recommended to keep a reference to the LongPoint instead of always calling this method
+ * for manual operations.
+ *
+ * @param labelValues the list of label values. The number of label values must be the same to
+ * that of the label keys passed to {@link MetricRegistry#addLongGauge}.
+ * @return a {@code LongPoint} the value of single gauge.
+ * @throws NullPointerException if {@code labelValues} is null OR any element of {@code
+ * labelValues} is null.
+ * @throws IllegalArgumentException if number of {@code labelValues}s are not equal to the label
+ * keys passed to {@link MetricRegistry#addLongGauge}.
+ * @since 0.17
+ */
+ public abstract LongPoint getOrCreateTimeSeries(List<LabelValue> labelValues);
+
+ /**
+ * Returns a {@code LongPoint} for a gauge with all labels not set, or default labels.
+ *
+ * @return a {@code LongPoint} for a gauge with all labels not set, or default labels.
+ * @since 0.17
+ */
+ public abstract LongPoint getDefaultTimeSeries();
+
+ /**
+ * Removes the {@code TimeSeries} from the gauge metric, if it is present. i.e. references to
+ * previous {@code LongPoint} objects are invalid (not part of the metric).
+ *
+ * @param labelValues the list of label values.
+ * @throws NullPointerException if {@code labelValues} is null.
+ * @since 0.17
+ */
+ public abstract void removeTimeSeries(List<LabelValue> labelValues);
+
+ /**
+ * Removes all {@code TimeSeries} from the gauge metric. i.e. references to all previous {@code
+ * LongPoint} objects are invalid (not part of the metric).
+ *
+ * @since 0.17
+ */
+ public abstract void clear();
+
+ /**
+ * Returns the no-op implementation of the {@code LongGauge}.
+ *
+ * @return the no-op implementation of the {@code LongGauge}.
+ * @since 0.17
+ */
+ static LongGauge newNoopLongGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ return NoopLongGauge.create(name, description, unit, labelKeys);
+ }
+
+ /**
+ * The value of a single point in the Gauge.TimeSeries.
+ *
+ * @since 0.17
+ */
+ public abstract static class LongPoint {
+
+ /**
+ * Adds the given value to the current value. The values can be negative.
+ *
+ * @param amt the value to add
+ * @since 0.17
+ */
+ public abstract void add(long amt);
+
+ /**
+ * Sets the given value.
+ *
+ * @param val the new value.
+ * @since 0.17
+ */
+ public abstract void set(long val);
+ }
+
+ /** No-op implementations of LongGauge class. */
+ private static final class NoopLongGauge extends LongGauge {
+ private final int labelKeysSize;
+
+ static NoopLongGauge create(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ return new NoopLongGauge(name, description, unit, labelKeys);
+ }
+
+ /** Creates a new {@code NoopLongPoint}. */
+ NoopLongGauge(String name, String description, String unit, List<LabelKey> labelKeys) {
+ labelKeysSize = labelKeys.size();
+ }
+
+ @Override
+ public NoopLongPoint getOrCreateTimeSeries(List<LabelValue> labelValues) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+ Utils.checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+ return NoopLongPoint.INSTANCE;
+ }
+
+ @Override
+ public NoopLongPoint getDefaultTimeSeries() {
+ return NoopLongPoint.INSTANCE;
+ }
+
+ @Override
+ public void removeTimeSeries(List<LabelValue> labelValues) {
+ Utils.checkNotNull(labelValues, "labelValues");
+ }
+
+ @Override
+ public void clear() {}
+
+ /** No-op implementations of LongPoint class. */
+ private static final class NoopLongPoint extends LongPoint {
+ private static final NoopLongPoint INSTANCE = new NoopLongPoint();
+
+ private NoopLongPoint() {}
+
+ @Override
+ public void add(long amt) {}
+
+ @Override
+ public void set(long val) {}
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/MetricRegistry.java b/api/src/main/java/io/opencensus/metrics/MetricRegistry.java
new file mode 100644
index 00000000..5be15594
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/MetricRegistry.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.internal.Utils;
+import java.util.List;
+
+/**
+ * Creates and manages your application's set of metrics. The default implementation of this creates
+ * a {@link io.opencensus.metrics.export.MetricProducer} and registers it to the global {@link
+ * io.opencensus.metrics.export.MetricProducerManager}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public abstract class MetricRegistry {
+ /**
+ * Builds a new long gauge to be added to the registry. This is more convenient form when you want
+ * to manually increase and decrease values as per your service requirements.
+ *
+ * @param name the name of the metric.
+ * @param description the description of the metric.
+ * @param unit the unit of the metric.
+ * @param labelKeys the list of the label keys.
+ * @throws NullPointerException if {@code labelKeys} is null OR any element of {@code labelKeys}
+ * is null OR {@code name}, {@code description}, {@code unit} is null.
+ * @throws IllegalArgumentException if different metric with the same name already registered.
+ * @since 0.17
+ */
+ @ExperimentalApi
+ public abstract LongGauge addLongGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys);
+
+ /**
+ * Builds a new double gauge to be added to the registry. This is more convenient form when you
+ * want to manually increase and decrease values as per your service requirements.
+ *
+ * @param name the name of the metric.
+ * @param description the description of the metric.
+ * @param unit the unit of the metric.
+ * @param labelKeys the list of the label keys.
+ * @throws NullPointerException if {@code labelKeys} is null OR any element of {@code labelKeys}
+ * is null OR {@code name}, {@code description}, {@code unit} is null.
+ * @throws IllegalArgumentException if different metric with the same name already registered.
+ * @since 0.17
+ */
+ @ExperimentalApi
+ public abstract DoubleGauge addDoubleGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys);
+
+ /**
+ * Builds a new derived long gauge to be added to the registry. This is more convenient form when
+ * you want to define a gauge by executing a {@link ToLongFunction} on an object.
+ *
+ * @param name the name of the metric.
+ * @param description the description of the metric.
+ * @param unit the unit of the metric.
+ * @param labelKeys the list of the label keys.
+ * @throws NullPointerException if {@code labelKeys} is null OR any element of {@code labelKeys}
+ * is null OR {@code name}, {@code description}, {@code unit} is null.
+ * @throws IllegalArgumentException if different metric with the same name already registered.
+ * @since 0.17
+ */
+ @ExperimentalApi
+ public abstract DerivedLongGauge addDerivedLongGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys);
+
+ /**
+ * Builds a new derived double gauge to be added to the registry. This is more convenient form
+ * when you want to define a gauge by executing a {@link ToDoubleFunction} on an object.
+ *
+ * @param name the name of the metric.
+ * @param description the description of the metric.
+ * @param unit the unit of the metric.
+ * @param labelKeys the list of the label keys.
+ * @throws NullPointerException if {@code labelKeys} is null OR any element of {@code labelKeys}
+ * is null OR {@code name}, {@code description}, {@code unit} is null.
+ * @throws IllegalArgumentException if different metric with the same name already registered.
+ * @since 0.17
+ */
+ @ExperimentalApi
+ public abstract DerivedDoubleGauge addDerivedDoubleGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys);
+
+ static MetricRegistry newNoopMetricRegistry() {
+ return new NoopMetricRegistry();
+ }
+
+ private static final class NoopMetricRegistry extends MetricRegistry {
+
+ @Override
+ public LongGauge addLongGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ return LongGauge.newNoopLongGauge(
+ Utils.checkNotNull(name, "name"),
+ Utils.checkNotNull(description, "description"),
+ Utils.checkNotNull(unit, "unit"),
+ labelKeys);
+ }
+
+ @Override
+ public DoubleGauge addDoubleGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ return DoubleGauge.newNoopDoubleGauge(
+ Utils.checkNotNull(name, "name"),
+ Utils.checkNotNull(description, "description"),
+ Utils.checkNotNull(unit, "unit"),
+ labelKeys);
+ }
+
+ @Override
+ public DerivedLongGauge addDerivedLongGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ return DerivedLongGauge.newNoopDerivedLongGauge(
+ Utils.checkNotNull(name, "name"),
+ Utils.checkNotNull(description, "description"),
+ Utils.checkNotNull(unit, "unit"),
+ labelKeys);
+ }
+
+ @Override
+ public DerivedDoubleGauge addDerivedDoubleGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ return DerivedDoubleGauge.newNoopDerivedDoubleGauge(
+ Utils.checkNotNull(name, "name"),
+ Utils.checkNotNull(description, "description"),
+ Utils.checkNotNull(unit, "unit"),
+ labelKeys);
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/Metrics.java b/api/src/main/java/io/opencensus/metrics/Metrics.java
new file mode 100644
index 00000000..920a4a88
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/Metrics.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Provider;
+import io.opencensus.metrics.export.ExportComponent;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Class for accessing the default {@link MetricsComponent}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public final class Metrics {
+ private static final Logger logger = Logger.getLogger(Metrics.class.getName());
+ private static final MetricsComponent metricsComponent =
+ loadMetricsComponent(MetricsComponent.class.getClassLoader());
+
+ /**
+ * Returns the global {@link ExportComponent}.
+ *
+ * @return the global {@code ExportComponent}.
+ * @since 0.17
+ */
+ public static ExportComponent getExportComponent() {
+ return metricsComponent.getExportComponent();
+ }
+
+ /**
+ * Returns the global {@link MetricRegistry}.
+ *
+ * <p>This {@code MetricRegistry} is already added to the global {@link
+ * io.opencensus.metrics.export.MetricProducerManager}.
+ *
+ * @return the global {@code MetricRegistry}.
+ * @since 0.17
+ */
+ public static MetricRegistry getMetricRegistry() {
+ return metricsComponent.getMetricRegistry();
+ }
+
+ // Any provider that may be used for MetricsComponent can be added here.
+ @DefaultVisibilityForTesting
+ static MetricsComponent loadMetricsComponent(@Nullable ClassLoader classLoader) {
+ try {
+ // Call Class.forName with literal string name of the class to help shading tools.
+ return Provider.createInstance(
+ Class.forName(
+ "io.opencensus.impl.metrics.MetricsComponentImpl", /*initialize=*/ true, classLoader),
+ MetricsComponent.class);
+ } catch (ClassNotFoundException e) {
+ logger.log(
+ Level.FINE,
+ "Couldn't load full implementation for MetricsComponent, now trying to load lite "
+ + "implementation.",
+ e);
+ }
+ try {
+ // Call Class.forName with literal string name of the class to help shading tools.
+ return Provider.createInstance(
+ Class.forName(
+ "io.opencensus.impllite.metrics.MetricsComponentImplLite",
+ /*initialize=*/ true,
+ classLoader),
+ MetricsComponent.class);
+ } catch (ClassNotFoundException e) {
+ logger.log(
+ Level.FINE,
+ "Couldn't load lite implementation for MetricsComponent, now using default "
+ + "implementation for MetricsComponent.",
+ e);
+ }
+ return MetricsComponent.newNoopMetricsComponent();
+ }
+
+ private Metrics() {}
+}
diff --git a/api/src/main/java/io/opencensus/metrics/MetricsComponent.java b/api/src/main/java/io/opencensus/metrics/MetricsComponent.java
new file mode 100644
index 00000000..3a992306
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/MetricsComponent.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.metrics.export.ExportComponent;
+
+/**
+ * Class that holds the implementation instance for {@link ExportComponent}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public abstract class MetricsComponent {
+
+ /**
+ * Returns the {@link ExportComponent} with the provided implementation. If no implementation is
+ * provided then no-op implementations will be used.
+ *
+ * @return the {@link ExportComponent} implementation.
+ * @since 0.17
+ */
+ public abstract ExportComponent getExportComponent();
+
+ /**
+ * Returns the {@link MetricRegistry} with the provided implementation.
+ *
+ * @return the {@link MetricRegistry} implementation.
+ * @since 0.17
+ */
+ public abstract MetricRegistry getMetricRegistry();
+
+ /**
+ * Returns an instance that contains no-op implementations for all the instances.
+ *
+ * @return an instance that contains no-op implementations for all the instances.
+ */
+ static MetricsComponent newNoopMetricsComponent() {
+ return new NoopMetricsComponent();
+ }
+
+ private static final class NoopMetricsComponent extends MetricsComponent {
+ private static final ExportComponent EXPORT_COMPONENT =
+ ExportComponent.newNoopExportComponent();
+ private static final MetricRegistry METRIC_REGISTRY = MetricRegistry.newNoopMetricRegistry();
+
+ @Override
+ public ExportComponent getExportComponent() {
+ return EXPORT_COMPONENT;
+ }
+
+ @Override
+ public MetricRegistry getMetricRegistry() {
+ return METRIC_REGISTRY;
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Distribution.java b/api/src/main/java/io/opencensus/metrics/export/Distribution.java
new file mode 100644
index 00000000..d55f101c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Distribution.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.Function;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link Distribution} contains summary statistics for a population of values. It optionally
+ * contains a histogram representing the distribution of those values across a set of buckets.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@AutoValue
+@Immutable
+public abstract class Distribution {
+
+ Distribution() {}
+
+ /**
+ * Creates a {@link Distribution}.
+ *
+ * @param count the count of the population values.
+ * @param sum the sum of the population values.
+ * @param sumOfSquaredDeviations the sum of squared deviations of the population values.
+ * @param bucketOptions the bucket options used to create a histogram for the distribution.
+ * @param buckets {@link Bucket}s of a histogram.
+ * @return a {@code Distribution}.
+ * @since 0.17
+ */
+ public static Distribution create(
+ long count,
+ double sum,
+ double sumOfSquaredDeviations,
+ BucketOptions bucketOptions,
+ List<Bucket> buckets) {
+ Utils.checkArgument(count >= 0, "count should be non-negative.");
+ Utils.checkArgument(
+ sumOfSquaredDeviations >= 0, "sum of squared deviations should be non-negative.");
+ if (count == 0) {
+ Utils.checkArgument(sum == 0, "sum should be 0 if count is 0.");
+ Utils.checkArgument(
+ sumOfSquaredDeviations == 0, "sum of squared deviations should be 0 if count is 0.");
+ }
+ Utils.checkNotNull(bucketOptions, "bucketOptions");
+ List<Bucket> bucketsCopy =
+ Collections.unmodifiableList(new ArrayList<Bucket>(Utils.checkNotNull(buckets, "buckets")));
+ Utils.checkListElementNotNull(bucketsCopy, "bucket");
+ return new AutoValue_Distribution(
+ count, sum, sumOfSquaredDeviations, bucketOptions, bucketsCopy);
+ }
+
+ /**
+ * Returns the aggregated count.
+ *
+ * @return the aggregated count.
+ * @since 0.17
+ */
+ public abstract long getCount();
+
+ /**
+ * Returns the aggregated sum.
+ *
+ * @return the aggregated sum.
+ * @since 0.17
+ */
+ public abstract double getSum();
+
+ /**
+ * Returns the aggregated sum of squared deviations.
+ *
+ * <p>The sum of squared deviations from the mean of the values in the population. For values x_i
+ * this is:
+ *
+ * <p>Sum[i=1..n]((x_i - mean)^2)
+ *
+ * <p>If count is zero then this field must be zero.
+ *
+ * @return the aggregated sum of squared deviations.
+ * @since 0.17
+ */
+ public abstract double getSumOfSquaredDeviations();
+
+ /**
+ * Returns bucket options used to create a histogram for the distribution.
+ *
+ * @return the {@code BucketOptions} associated with the {@code Distribution}, or {@code null} if
+ * there isn't one.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract BucketOptions getBucketOptions();
+
+ /**
+ * Returns the aggregated histogram {@link Bucket}s.
+ *
+ * @return the aggregated histogram buckets.
+ * @since 0.17
+ */
+ public abstract List<Bucket> getBuckets();
+
+ /**
+ * The bucket options used to create a histogram for the distribution.
+ *
+ * @since 0.17
+ */
+ @Immutable
+ public abstract static class BucketOptions {
+
+ private BucketOptions() {}
+
+ /**
+ * Returns a {@link ExplicitOptions}.
+ *
+ * <p>The bucket boundaries for that histogram are described by bucket_bounds. This defines
+ * size(bucket_bounds) + 1 (= N) buckets. The boundaries for bucket index i are:
+ *
+ * <ul>
+ * <li>{@code [0, bucket_bounds[i]) for i == 0}
+ * <li>{@code [bucket_bounds[i-1], bucket_bounds[i]) for 0 < i < N-1}
+ * <li>{@code [bucket_bounds[i-1], +infinity) for i == N-1}
+ * </ul>
+ *
+ * <p>If bucket_bounds has no elements (zero size), then there is no histogram associated with
+ * the Distribution. If bucket_bounds has only one element, there are no finite buckets, and
+ * that single element is the common boundary of the overflow and underflow buckets. The values
+ * must be monotonically increasing.
+ *
+ * @param bucketBoundaries the bucket boundaries of a distribution (given explicitly). The
+ * values must be strictly increasing and should be positive values.
+ * @return a {@code ExplicitOptions} {@code BucketOptions}.
+ * @since 0.17
+ */
+ public static BucketOptions explicitOptions(List<Double> bucketBoundaries) {
+ return ExplicitOptions.create(bucketBoundaries);
+ }
+
+ /**
+ * Applies the given match function to the underlying BucketOptions.
+ *
+ * @param explicitFunction the function that should be applied if the BucketOptions has type
+ * {@code ExplicitOptions}.
+ * @param defaultFunction the function that should be applied if the BucketOptions has a type
+ * that was added after this {@code match} method was added to the API. See {@link
+ * io.opencensus.common.Functions} for some common functions for handling unknown types.
+ * @return the result of the function applied to the underlying BucketOptions.
+ * @since 0.17
+ */
+ public abstract <T> T match(
+ Function<? super ExplicitOptions, T> explicitFunction,
+ Function<? super BucketOptions, T> defaultFunction);
+
+ /** A Bucket with explicit bounds {@link BucketOptions}. */
+ @AutoValue
+ @Immutable
+ public abstract static class ExplicitOptions extends BucketOptions {
+
+ ExplicitOptions() {}
+
+ @Override
+ public final <T> T match(
+ Function<? super ExplicitOptions, T> explicitFunction,
+ Function<? super BucketOptions, T> defaultFunction) {
+ return explicitFunction.apply(this);
+ }
+
+ /**
+ * Creates a {@link ExplicitOptions}.
+ *
+ * @param bucketBoundaries the bucket boundaries of a distribution (given explicitly). The
+ * values must be strictly increasing and should be positive.
+ * @return a {@code ExplicitOptions}.
+ * @since 0.17
+ */
+ private static ExplicitOptions create(List<Double> bucketBoundaries) {
+ Utils.checkNotNull(bucketBoundaries, "bucketBoundaries");
+ List<Double> bucketBoundariesCopy =
+ Collections.unmodifiableList(new ArrayList<Double>(bucketBoundaries));
+ checkBucketBoundsAreSorted(bucketBoundariesCopy);
+ return new AutoValue_Distribution_BucketOptions_ExplicitOptions(bucketBoundariesCopy);
+ }
+
+ private static void checkBucketBoundsAreSorted(List<Double> bucketBoundaries) {
+ if (bucketBoundaries.size() >= 1) {
+ double previous = Utils.checkNotNull(bucketBoundaries.get(0), "bucketBoundary");
+ Utils.checkArgument(previous > 0, "bucket boundary should be > 0");
+ for (int i = 1; i < bucketBoundaries.size(); i++) {
+ double next = Utils.checkNotNull(bucketBoundaries.get(i), "bucketBoundary");
+ Utils.checkArgument(previous < next, "bucket boundaries not sorted.");
+ previous = next;
+ }
+ }
+ }
+
+ /**
+ * Returns the bucket boundaries of this distribution.
+ *
+ * @return the bucket boundaries of this distribution.
+ * @since 0.17
+ */
+ public abstract List<Double> getBucketBoundaries();
+ }
+ }
+
+ /**
+ * The histogram bucket of the population values.
+ *
+ * @since 0.17
+ */
+ @AutoValue
+ @Immutable
+ public abstract static class Bucket {
+
+ Bucket() {}
+
+ /**
+ * Creates a {@link Bucket}.
+ *
+ * @param count the number of values in each bucket of the histogram.
+ * @return a {@code Bucket}.
+ * @since 0.17
+ */
+ public static Bucket create(long count) {
+ Utils.checkArgument(count >= 0, "bucket count should be non-negative.");
+ return new AutoValue_Distribution_Bucket(count, null);
+ }
+
+ /**
+ * Creates a {@link Bucket} with an {@link Exemplar}.
+ *
+ * @param count the number of values in each bucket of the histogram.
+ * @param exemplar the {@code Exemplar} of this {@code Bucket}.
+ * @return a {@code Bucket}.
+ * @since 0.17
+ */
+ public static Bucket create(long count, Exemplar exemplar) {
+ Utils.checkArgument(count >= 0, "bucket count should be non-negative.");
+ Utils.checkNotNull(exemplar, "exemplar");
+ return new AutoValue_Distribution_Bucket(count, exemplar);
+ }
+
+ /**
+ * Returns the number of values in each bucket of the histogram.
+ *
+ * @return the number of values in each bucket of the histogram.
+ * @since 0.17
+ */
+ public abstract long getCount();
+
+ /**
+ * Returns the {@link Exemplar} associated with the {@link Bucket}, or {@code null} if there
+ * isn't one.
+ *
+ * @return the {@code Exemplar} associated with the {@code Bucket}, or {@code null} if there
+ * isn't one.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Exemplar getExemplar();
+ }
+
+ /**
+ * An example point that may be used to annotate aggregated distribution values, associated with a
+ * histogram bucket.
+ *
+ * @since 0.17
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class Exemplar {
+
+ Exemplar() {}
+
+ /**
+ * Returns value of the {@link Exemplar} point.
+ *
+ * @return value of the {@code Exemplar} point.
+ * @since 0.17
+ */
+ public abstract double getValue();
+
+ /**
+ * Returns the time that this {@link Exemplar}'s value was recorded.
+ *
+ * @return the time that this {@code Exemplar}'s value was recorded.
+ * @since 0.17
+ */
+ public abstract Timestamp getTimestamp();
+
+ /**
+ * Returns the contextual information about the example value, represented as a string map.
+ *
+ * @return the contextual information about the example value.
+ * @since 0.17
+ */
+ public abstract Map<String, String> getAttachments();
+
+ /**
+ * Creates an {@link Exemplar}.
+ *
+ * @param value value of the {@link Exemplar} point.
+ * @param timestamp the time that this {@code Exemplar}'s value was recorded.
+ * @param attachments the contextual information about the example value.
+ * @return an {@code Exemplar}.
+ * @since 0.17
+ */
+ public static Exemplar create(
+ double value, Timestamp timestamp, Map<String, String> attachments) {
+ Utils.checkNotNull(attachments, "attachments");
+ Map<String, String> attachmentsCopy =
+ Collections.unmodifiableMap(new HashMap<String, String>(attachments));
+ for (Entry<String, String> entry : attachmentsCopy.entrySet()) {
+ Utils.checkNotNull(entry.getKey(), "key of attachments");
+ Utils.checkNotNull(entry.getValue(), "value of attachments");
+ }
+ return new AutoValue_Distribution_Exemplar(value, timestamp, attachmentsCopy);
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/ExportComponent.java b/api/src/main/java/io/opencensus/metrics/export/ExportComponent.java
new file mode 100644
index 00000000..11e1fdbd
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/ExportComponent.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import io.opencensus.common.ExperimentalApi;
+
+/**
+ * Class that holds the implementation instance for {@link MetricProducerManager}.
+ *
+ * <p>Unless otherwise noted all methods (on component) results are cacheable.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public abstract class ExportComponent {
+ /**
+ * Returns the no-op implementation of the {@code ExportComponent}.
+ *
+ * @return the no-op implementation of the {@code ExportComponent}.
+ * @since 0.17
+ */
+ public static ExportComponent newNoopExportComponent() {
+ return new NoopExportComponent();
+ }
+
+ /**
+ * Returns the global {@link MetricProducerManager} which can be used to register handlers to
+ * export all the recorded metrics.
+ *
+ * @return the implementation of the {@code MetricExporter} or no-op if no implementation linked
+ * in the binary.
+ * @since 0.17
+ */
+ public abstract MetricProducerManager getMetricProducerManager();
+
+ private static final class NoopExportComponent extends ExportComponent {
+
+ private static final MetricProducerManager METRIC_PRODUCER_MANAGER =
+ MetricProducerManager.newNoopMetricProducerManager();
+
+ @Override
+ public MetricProducerManager getMetricProducerManager() {
+ return METRIC_PRODUCER_MANAGER;
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Metric.java b/api/src/main/java/io/opencensus/metrics/export/Metric.java
new file mode 100644
index 00000000..7b93fc86
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Metric.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import io.opencensus.metrics.export.Value.ValueDistribution;
+import io.opencensus.metrics.export.Value.ValueDouble;
+import io.opencensus.metrics.export.Value.ValueLong;
+import io.opencensus.metrics.export.Value.ValueSummary;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A {@link Metric} with one or more {@link TimeSeries}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class Metric {
+
+ Metric() {}
+
+ /**
+ * Creates a {@link Metric}.
+ *
+ * @param metricDescriptor the {@link MetricDescriptor}.
+ * @param timeSeriesList the {@link TimeSeries} list for this metric.
+ * @return a {@code Metric}.
+ * @since 0.17
+ */
+ public static Metric create(MetricDescriptor metricDescriptor, List<TimeSeries> timeSeriesList) {
+ Utils.checkListElementNotNull(
+ Utils.checkNotNull(timeSeriesList, "timeSeriesList"), "timeSeries");
+ return createInternal(
+ metricDescriptor, Collections.unmodifiableList(new ArrayList<TimeSeries>(timeSeriesList)));
+ }
+
+ /**
+ * Creates a {@link Metric}.
+ *
+ * @param metricDescriptor the {@link MetricDescriptor}.
+ * @param timeSeries the single {@link TimeSeries} for this metric.
+ * @return a {@code Metric}.
+ * @since 0.17
+ */
+ public static Metric createWithOneTimeSeries(
+ MetricDescriptor metricDescriptor, TimeSeries timeSeries) {
+ return createInternal(
+ metricDescriptor, Collections.singletonList(Utils.checkNotNull(timeSeries, "timeSeries")));
+ }
+
+ /**
+ * Creates a {@link Metric}.
+ *
+ * @param metricDescriptor the {@link MetricDescriptor}.
+ * @param timeSeriesList the {@link TimeSeries} list for this metric.
+ * @return a {@code Metric}.
+ * @since 0.17
+ */
+ private static Metric createInternal(
+ MetricDescriptor metricDescriptor, List<TimeSeries> timeSeriesList) {
+ Utils.checkNotNull(metricDescriptor, "metricDescriptor");
+ checkTypeMatch(metricDescriptor.getType(), timeSeriesList);
+ return new AutoValue_Metric(metricDescriptor, timeSeriesList);
+ }
+
+ /**
+ * Returns the {@link MetricDescriptor} of this metric.
+ *
+ * @return the {@code MetricDescriptor} of this metric.
+ * @since 0.17
+ */
+ public abstract MetricDescriptor getMetricDescriptor();
+
+ /**
+ * Returns the {@link TimeSeries} list for this metric.
+ *
+ * <p>The type of the {@link TimeSeries#getPoints()} must match {@link MetricDescriptor.Type}.
+ *
+ * @return the {@code TimeSeriesList} for this metric.
+ * @since 0.17
+ */
+ public abstract List<TimeSeries> getTimeSeriesList();
+
+ private static void checkTypeMatch(MetricDescriptor.Type type, List<TimeSeries> timeSeriesList) {
+ for (TimeSeries timeSeries : timeSeriesList) {
+ for (Point point : timeSeries.getPoints()) {
+ Value value = point.getValue();
+ String valueClassName = "";
+ if (value.getClass().getSuperclass() != null) { // work around nullness check
+ // AutoValue classes should always have a super class.
+ valueClassName = value.getClass().getSuperclass().getSimpleName();
+ }
+ switch (type) {
+ case GAUGE_INT64:
+ case CUMULATIVE_INT64:
+ Utils.checkArgument(
+ value instanceof ValueLong, "Type mismatch: %s, %s.", type, valueClassName);
+ break;
+ case CUMULATIVE_DOUBLE:
+ case GAUGE_DOUBLE:
+ Utils.checkArgument(
+ value instanceof ValueDouble, "Type mismatch: %s, %s.", type, valueClassName);
+ break;
+ case GAUGE_DISTRIBUTION:
+ case CUMULATIVE_DISTRIBUTION:
+ Utils.checkArgument(
+ value instanceof ValueDistribution, "Type mismatch: %s, %s.", type, valueClassName);
+ break;
+ case SUMMARY:
+ Utils.checkArgument(
+ value instanceof ValueSummary, "Type mismatch: %s, %s.", type, valueClassName);
+ }
+ }
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/MetricDescriptor.java b/api/src/main/java/io/opencensus/metrics/export/MetricDescriptor.java
new file mode 100644
index 00000000..a4629f8e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/MetricDescriptor.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import io.opencensus.metrics.LabelKey;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link MetricDescriptor} defines a {@code Metric} type and its schema.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class MetricDescriptor {
+
+ MetricDescriptor() {}
+
+ /**
+ * Creates a {@link MetricDescriptor}.
+ *
+ * @param name name of {@code MetricDescriptor}.
+ * @param description description of {@code MetricDescriptor}.
+ * @param unit the metric unit.
+ * @param type type of {@code MetricDescriptor}.
+ * @param labelKeys the label keys associated with the {@code MetricDescriptor}.
+ * @return a {@code MetricDescriptor}.
+ * @since 0.17
+ */
+ public static MetricDescriptor create(
+ String name, String description, String unit, Type type, List<LabelKey> labelKeys) {
+ Utils.checkNotNull(labelKeys, "labelKeys");
+ Utils.checkListElementNotNull(labelKeys, "labelKey");
+ return new AutoValue_MetricDescriptor(
+ name,
+ description,
+ unit,
+ type,
+ Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+ }
+
+ /**
+ * Returns the metric descriptor name.
+ *
+ * @return the metric descriptor name.
+ * @since 0.17
+ */
+ public abstract String getName();
+
+ /**
+ * Returns the description of this metric descriptor.
+ *
+ * @return the description of this metric descriptor.
+ * @since 0.17
+ */
+ public abstract String getDescription();
+
+ /**
+ * Returns the unit of this metric descriptor.
+ *
+ * @return the unit of this metric descriptor.
+ * @since 0.17
+ */
+ public abstract String getUnit();
+
+ /**
+ * Returns the type of this metric descriptor.
+ *
+ * @return the type of this metric descriptor.
+ * @since 0.17
+ */
+ public abstract Type getType();
+
+ /**
+ * Returns the label keys associated with this metric descriptor.
+ *
+ * @return the label keys associated with this metric descriptor.
+ * @since 0.17
+ */
+ public abstract List<LabelKey> getLabelKeys();
+
+ /**
+ * The kind of metric. It describes how the data is reported.
+ *
+ * <p>A gauge is an instantaneous measurement of a value.
+ *
+ * <p>A cumulative measurement is a value accumulated over a time interval. In a time series,
+ * cumulative measurements should have the same start time and increasing end times, until an
+ * event resets the cumulative value to zero and sets a new start time for the following points.
+ *
+ * @since 0.17
+ */
+ public enum Type {
+
+ /**
+ * An instantaneous measurement of an int64 value.
+ *
+ * @since 0.17
+ */
+ GAUGE_INT64,
+
+ /**
+ * An instantaneous measurement of a double value.
+ *
+ * @since 0.17
+ */
+ GAUGE_DOUBLE,
+
+ /**
+ * An instantaneous measurement of a distribution value. The count and sum can go both up and
+ * down. Used in scenarios like a snapshot of time the current items in a queue have spent
+ * there.
+ *
+ * @since 0.17
+ */
+ GAUGE_DISTRIBUTION,
+
+ /**
+ * An cumulative measurement of an int64 value.
+ *
+ * @since 0.17
+ */
+ CUMULATIVE_INT64,
+
+ /**
+ * An cumulative measurement of a double value.
+ *
+ * @since 0.17
+ */
+ CUMULATIVE_DOUBLE,
+
+ /**
+ * An cumulative measurement of a distribution value. The count and sum can only go up, if
+ * resets then the start_time should also be reset.
+ *
+ * @since 0.17
+ */
+ CUMULATIVE_DISTRIBUTION,
+
+ /**
+ * Some frameworks implemented DISTRIBUTION as a summary of observations (usually things like
+ * request durations and response sizes). While it also provides a total count of observations
+ * and a sum of all observed values, it calculates configurable quantiles over a sliding time
+ * window.
+ *
+ * <p>This is not recommended, since it cannot be aggregated.
+ *
+ * @since 0.17
+ */
+ SUMMARY,
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/MetricProducer.java b/api/src/main/java/io/opencensus/metrics/export/MetricProducer.java
new file mode 100644
index 00000000..739a0a9f
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/MetricProducer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import io.opencensus.common.ExperimentalApi;
+import java.util.Collection;
+
+/**
+ * A {@link Metric} producer that can be registered for exporting using {@link
+ * MetricProducerManager}.
+ *
+ * <p>All implementation MUST be thread-safe.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+public abstract class MetricProducer {
+
+ /**
+ * Returns a collection of produced {@link Metric}s to be exported.
+ *
+ * @return a collection of produced {@link Metric}s to be exported.
+ * @since 0.17
+ */
+ public abstract Collection<Metric> getMetrics();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/MetricProducerManager.java b/api/src/main/java/io/opencensus/metrics/export/MetricProducerManager.java
new file mode 100644
index 00000000..304d9294
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/MetricProducerManager.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import java.util.Collections;
+import java.util.Set;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Keeps a set of {@link MetricProducer} that is used by exporters to determine the metrics that
+ * need to be exported.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@ThreadSafe
+public abstract class MetricProducerManager {
+
+ /**
+ * Adds the {@link MetricProducer} to the manager if it is not already present.
+ *
+ * @param metricProducer the {@code MetricProducer} to be added to the manager.
+ * @since 0.17
+ */
+ public abstract void add(MetricProducer metricProducer);
+
+ /**
+ * Removes the {@link MetricProducer} to the manager if it is present.
+ *
+ * @param metricProducer the {@code MetricProducer} to be removed from the manager.
+ * @since 0.17
+ */
+ public abstract void remove(MetricProducer metricProducer);
+
+ /**
+ * Returns all registered {@link MetricProducer}s that should be exported.
+ *
+ * <p>This method should be used by any metrics exporter that automatically exports data for
+ * {@code MetricProducer} registered with the {@code MetricProducerManager}.
+ *
+ * @return all registered {@code MetricProducer}s that should be exported.
+ * @since 0.17
+ */
+ public abstract Set<MetricProducer> getAllMetricProducer();
+
+ /**
+ * Returns a no-op implementation for {@link MetricProducerManager}.
+ *
+ * @return a no-op implementation for {@code MetricProducerManager}.
+ */
+ static MetricProducerManager newNoopMetricProducerManager() {
+ return new NoopMetricProducerManager();
+ }
+
+ private static final class NoopMetricProducerManager extends MetricProducerManager {
+
+ @Override
+ public void add(MetricProducer metricProducer) {
+ Utils.checkNotNull(metricProducer, "metricProducer");
+ }
+
+ @Override
+ public void remove(MetricProducer metricProducer) {
+ Utils.checkNotNull(metricProducer, "metricProducer");
+ }
+
+ @Override
+ public Set<MetricProducer> getAllMetricProducer() {
+ return Collections.emptySet();
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Point.java b/api/src/main/java/io/opencensus/metrics/export/Point.java
new file mode 100644
index 00000000..1f382f9b
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Point.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A timestamped measurement of a {@code TimeSeries}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@AutoValue
+@Immutable
+public abstract class Point {
+
+ Point() {}
+
+ /**
+ * Creates a {@link Point}.
+ *
+ * @param value the {@link Value} of this {@link Point}.
+ * @param timestamp the {@link Timestamp} when this {@link Point} was recorded.
+ * @return a {@code Point}.
+ * @since 0.17
+ */
+ public static Point create(Value value, Timestamp timestamp) {
+ return new AutoValue_Point(value, timestamp);
+ }
+
+ /**
+ * Returns the {@link Value}.
+ *
+ * @return the {@code Value}.
+ * @since 0.17
+ */
+ public abstract Value getValue();
+
+ /**
+ * Returns the {@link Timestamp} when this {@link Point} was recorded.
+ *
+ * @return the {@code Timestamp}.
+ * @since 0.17
+ */
+ public abstract Timestamp getTimestamp();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Summary.java b/api/src/main/java/io/opencensus/metrics/export/Summary.java
new file mode 100644
index 00000000..c82ca961
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Summary.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Implementation of the {@link Distribution} as a summary of observations.
+ *
+ * <p>This is not recommended, since it cannot be aggregated.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@AutoValue
+@Immutable
+public abstract class Summary {
+ Summary() {}
+
+ /**
+ * Creates a {@link Summary}.
+ *
+ * @param count the count of the population values.
+ * @param sum the sum of the population values.
+ * @param snapshot bucket boundaries of a histogram.
+ * @return a {@code Summary} with the given values.
+ * @since 0.17
+ */
+ public static Summary create(@Nullable Long count, @Nullable Double sum, Snapshot snapshot) {
+ checkCountAndSum(count, sum);
+ Utils.checkNotNull(snapshot, "snapshot");
+ return new AutoValue_Summary(count, sum, snapshot);
+ }
+
+ /**
+ * Returns the aggregated count. If not available returns {@code null}.
+ *
+ * @return the aggregated count.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Long getCount();
+
+ /**
+ * Returns the aggregated sum. If not available returns {@code null}.
+ *
+ * @return the aggregated sum.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Double getSum();
+
+ /**
+ * Returns the {@link Snapshot}.
+ *
+ * @return the {@code Snapshot}.
+ * @since 0.17
+ */
+ public abstract Snapshot getSnapshot();
+
+ /**
+ * Represents the summary observation of the recorded events over a sliding time window.
+ *
+ * @since 0.17
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class Snapshot {
+ /**
+ * Returns the number of values in this {@code Snapshot}. If not available returns {@code null}.
+ *
+ * @return the number of values in this {@code Snapshot}.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Long getCount();
+
+ /**
+ * Returns the sum of values in this {@code Snapshot}. If not available returns {@code null}.
+ *
+ * @return the sum of values in this {@code Snapshot}.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Double getSum();
+
+ /**
+ * Returns the list of {@code ValueAtPercentile}s in this {@code Snapshot}.
+ *
+ * @return the list of {@code ValueAtPercentile}s in this {@code Snapshot}.
+ * @since 0.17
+ */
+ public abstract List<ValueAtPercentile> getValueAtPercentiles();
+
+ /**
+ * Creates a {@link Snapshot}.
+ *
+ * @param count the number of values in this {@code Snapshot}.
+ * @param sum the number of values in this {@code Snapshot}.
+ * @param valueAtPercentiles the list of {@code ValueAtPercentile}.
+ * @return a {@code Snapshot} with the given values.
+ * @since 0.17
+ */
+ public static Snapshot create(
+ @Nullable Long count, @Nullable Double sum, List<ValueAtPercentile> valueAtPercentiles) {
+ checkCountAndSum(count, sum);
+ Utils.checkNotNull(valueAtPercentiles, "valueAtPercentiles");
+ Utils.checkListElementNotNull(valueAtPercentiles, "value in valueAtPercentiles");
+ return new AutoValue_Summary_Snapshot(
+ count,
+ sum,
+ Collections.unmodifiableList(new ArrayList<ValueAtPercentile>(valueAtPercentiles)));
+ }
+
+ /**
+ * Represents the value at a given percentile of a distribution.
+ *
+ * @since 0.17
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class ValueAtPercentile {
+ /**
+ * Returns the percentile in this {@code ValueAtPercentile}.
+ *
+ * <p>Must be in the interval (0.0, 100.0].
+ *
+ * @return the percentile in this {@code ValueAtPercentile}.
+ * @since 0.17
+ */
+ public abstract double getPercentile();
+
+ /**
+ * Returns the value in this {@code ValueAtPercentile}.
+ *
+ * @return the value in this {@code ValueAtPercentile}.
+ * @since 0.17
+ */
+ public abstract double getValue();
+
+ /**
+ * Creates a {@link ValueAtPercentile}.
+ *
+ * @param percentile the percentile in this {@code ValueAtPercentile}.
+ * @param value the value in this {@code ValueAtPercentile}.
+ * @return a {@code ValueAtPercentile} with the given values.
+ * @since 0.17
+ */
+ public static ValueAtPercentile create(double percentile, double value) {
+ Utils.checkArgument(
+ 0 < percentile && percentile <= 100.0,
+ "percentile must be in the interval (0.0, 100.0]");
+ Utils.checkArgument(value >= 0, "value must be non-negative");
+ return new AutoValue_Summary_Snapshot_ValueAtPercentile(percentile, value);
+ }
+ }
+ }
+
+ private static void checkCountAndSum(@Nullable Long count, @Nullable Double sum) {
+ Utils.checkArgument(count == null || count >= 0, "count must be non-negative.");
+ Utils.checkArgument(sum == null || sum >= 0, "sum must be non-negative.");
+ if (count != null && count == 0) {
+ Utils.checkArgument(sum == null || sum == 0, "sum must be 0 if count is 0.");
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/TimeSeries.java b/api/src/main/java/io/opencensus/metrics/export/TimeSeries.java
new file mode 100644
index 00000000..bfaeae98
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/TimeSeries.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A collection of data points that describes the time-varying values of a {@code Metric}.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@Immutable
+@AutoValue
+public abstract class TimeSeries {
+
+ TimeSeries() {}
+
+ /**
+ * Creates a {@link TimeSeries}.
+ *
+ * @param labelValues the {@code LabelValue}s that uniquely identify this {@code TimeSeries}.
+ * @param points the data {@code Point}s of this {@code TimeSeries}.
+ * @param startTimestamp the start {@code Timestamp} of this {@code TimeSeries}. Must be non-null
+ * for cumulative {@code Point}s.
+ * @return a {@code TimeSeries}.
+ * @since 0.17
+ */
+ public static TimeSeries create(
+ List<LabelValue> labelValues, List<Point> points, @Nullable Timestamp startTimestamp) {
+ Utils.checkNotNull(points, "points");
+ Utils.checkListElementNotNull(points, "point");
+ return createInternal(
+ labelValues, Collections.unmodifiableList(new ArrayList<Point>(points)), startTimestamp);
+ }
+
+ /**
+ * Creates a {@link TimeSeries}.
+ *
+ * @param labelValues the {@code LabelValue}s that uniquely identify this {@code TimeSeries}.
+ * @param point the single data {@code Point} of this {@code TimeSeries}.
+ * @param startTimestamp the start {@code Timestamp} of this {@code TimeSeries}. Must be non-null
+ * for cumulative {@code Point}s.
+ * @return a {@code TimeSeries}.
+ * @since 0.17
+ */
+ public static TimeSeries createWithOnePoint(
+ List<LabelValue> labelValues, Point point, @Nullable Timestamp startTimestamp) {
+ Utils.checkNotNull(point, "point");
+ return createInternal(labelValues, Collections.singletonList(point), startTimestamp);
+ }
+
+ /**
+ * Creates a {@link TimeSeries}.
+ *
+ * @param labelValues the {@code LabelValue}s that uniquely identify this {@code TimeSeries}.
+ * @param points the data {@code Point}s of this {@code TimeSeries}.
+ * @param startTimestamp the start {@code Timestamp} of this {@code TimeSeries}. Must be non-null
+ * for cumulative {@code Point}s.
+ * @return a {@code TimeSeries}.
+ */
+ private static TimeSeries createInternal(
+ List<LabelValue> labelValues, List<Point> points, @Nullable Timestamp startTimestamp) {
+ // Fail fast on null lists to prevent NullPointerException when copying the lists.
+ Utils.checkNotNull(labelValues, "labelValues");
+ Utils.checkListElementNotNull(labelValues, "labelValue");
+ return new AutoValue_TimeSeries(
+ Collections.unmodifiableList(new ArrayList<LabelValue>(labelValues)),
+ points,
+ startTimestamp);
+ }
+
+ /**
+ * Returns the set of {@link LabelValue}s that uniquely identify this {@link TimeSeries}.
+ *
+ * <p>Apply to all {@link Point}s.
+ *
+ * <p>The order of {@link LabelValue}s must match that of {@link LabelKey}s in the {@code
+ * MetricDescriptor}.
+ *
+ * @return the {@code LabelValue}s.
+ * @since 0.17
+ */
+ public abstract List<LabelValue> getLabelValues();
+
+ /**
+ * Returns the data {@link Point}s of this {@link TimeSeries}.
+ *
+ * @return the data {@code Point}s.
+ * @since 0.17
+ */
+ public abstract List<Point> getPoints();
+
+ /**
+ * Returns the start {@link Timestamp} of this {@link TimeSeries} if the {@link Point}s are
+ * cumulative, or {@code null} if the {@link Point}s are gauge.
+ *
+ * @return the start {@code Timestamp} or {@code null}.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Timestamp getStartTimestamp();
+}
diff --git a/api/src/main/java/io/opencensus/metrics/export/Value.java b/api/src/main/java/io/opencensus/metrics/export/Value.java
new file mode 100644
index 00000000..00a939c0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/export/Value.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.common.Function;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The actual point value for a {@link Point}.
+ *
+ * <p>Currently there are three types of {@link Value}:
+ *
+ * <ul>
+ * <li>{@code double}
+ * <li>{@code long}
+ * <li>{@link Distribution}
+ * </ul>
+ *
+ * <p>Each {@link Point} contains exactly one of the three {@link Value} types.
+ *
+ * @since 0.17
+ */
+@ExperimentalApi
+@Immutable
+public abstract class Value {
+
+ Value() {}
+
+ /**
+ * Returns a double {@link Value}.
+ *
+ * @param value value in double.
+ * @return a double {@code Value}.
+ * @since 0.17
+ */
+ public static Value doubleValue(double value) {
+ return ValueDouble.create(value);
+ }
+
+ /**
+ * Returns a long {@link Value}.
+ *
+ * @param value value in long.
+ * @return a long {@code Value}.
+ * @since 0.17
+ */
+ public static Value longValue(long value) {
+ return ValueLong.create(value);
+ }
+
+ /**
+ * Returns a {@link Distribution} {@link Value}.
+ *
+ * @param value value in {@link Distribution}.
+ * @return a {@code Distribution} {@code Value}.
+ * @since 0.17
+ */
+ public static Value distributionValue(Distribution value) {
+ return ValueDistribution.create(value);
+ }
+
+ /**
+ * Returns a {@link Summary} {@link Value}.
+ *
+ * @param value value in {@link Summary}.
+ * @return a {@code Summary} {@code Value}.
+ * @since 0.17
+ */
+ public static Value summaryValue(Summary value) {
+ return ValueSummary.create(value);
+ }
+
+ /**
+ * Applies the given match function to the underlying data type.
+ *
+ * @since 0.17
+ */
+ public abstract <T> T match(
+ Function<? super Double, T> doubleFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Distribution, T> distributionFunction,
+ Function<? super Summary, T> summaryFunction,
+ Function<? super Value, T> defaultFunction);
+
+ /** A 64-bit double-precision floating-point {@link Value}. */
+ @AutoValue
+ @Immutable
+ abstract static class ValueDouble extends Value {
+
+ ValueDouble() {}
+
+ @Override
+ public final <T> T match(
+ Function<? super Double, T> doubleFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Distribution, T> distributionFunction,
+ Function<? super Summary, T> summaryFunction,
+ Function<? super Value, T> defaultFunction) {
+ return doubleFunction.apply(getValue());
+ }
+
+ /**
+ * Creates a {@link ValueDouble}.
+ *
+ * @param value the value in double.
+ * @return a {@code ValueDouble}.
+ */
+ static ValueDouble create(double value) {
+ return new AutoValue_Value_ValueDouble(value);
+ }
+
+ /**
+ * Returns the double value.
+ *
+ * @return the double value.
+ */
+ abstract double getValue();
+ }
+
+ /** A 64-bit integer {@link Value}. */
+ @AutoValue
+ @Immutable
+ abstract static class ValueLong extends Value {
+
+ ValueLong() {}
+
+ @Override
+ public final <T> T match(
+ Function<? super Double, T> doubleFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Distribution, T> distributionFunction,
+ Function<? super Summary, T> summaryFunction,
+ Function<? super Value, T> defaultFunction) {
+ return longFunction.apply(getValue());
+ }
+
+ /**
+ * Creates a {@link ValueLong}.
+ *
+ * @param value the value in long.
+ * @return a {@code ValueLong}.
+ */
+ static ValueLong create(long value) {
+ return new AutoValue_Value_ValueLong(value);
+ }
+
+ /**
+ * Returns the long value.
+ *
+ * @return the long value.
+ */
+ abstract long getValue();
+ }
+
+ /**
+ * {@link ValueDistribution} contains summary statistics for a population of values. It optionally
+ * contains a histogram representing the distribution of those values across a set of buckets.
+ */
+ @AutoValue
+ @Immutable
+ abstract static class ValueDistribution extends Value {
+
+ ValueDistribution() {}
+
+ @Override
+ public final <T> T match(
+ Function<? super Double, T> doubleFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Distribution, T> distributionFunction,
+ Function<? super Summary, T> summaryFunction,
+ Function<? super Value, T> defaultFunction) {
+ return distributionFunction.apply(getValue());
+ }
+
+ /**
+ * Creates a {@link ValueDistribution}.
+ *
+ * @param value the {@link Distribution} value.
+ * @return a {@code ValueDistribution}.
+ */
+ static ValueDistribution create(Distribution value) {
+ return new AutoValue_Value_ValueDistribution(value);
+ }
+
+ /**
+ * Returns the {@link Distribution} value.
+ *
+ * @return the {@code Distribution} value.
+ */
+ abstract Distribution getValue();
+ }
+
+ /**
+ * {@link ValueSummary} contains a snapshot representing values calculated over an arbitrary time
+ * window.
+ */
+ @AutoValue
+ @Immutable
+ abstract static class ValueSummary extends Value {
+
+ ValueSummary() {}
+
+ @Override
+ public final <T> T match(
+ Function<? super Double, T> doubleFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Distribution, T> distributionFunction,
+ Function<? super Summary, T> summaryFunction,
+ Function<? super Value, T> defaultFunction) {
+ return summaryFunction.apply(getValue());
+ }
+
+ /**
+ * Creates a {@link ValueSummary}.
+ *
+ * @param value the {@link Summary} value.
+ * @return a {@code ValueSummary}.
+ */
+ static ValueSummary create(Summary value) {
+ return new AutoValue_Value_ValueSummary(value);
+ }
+
+ /**
+ * Returns the {@link Summary} value.
+ *
+ * @return the {@code Summary} value.
+ */
+ abstract Summary getValue();
+ }
+}
diff --git a/api/src/main/java/io/opencensus/metrics/package-info.java b/api/src/main/java/io/opencensus/metrics/package-info.java
new file mode 100644
index 00000000..33eadf0c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/metrics/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/**
+ * This package describes the Metrics data model. Metrics are a data model for what stats exporters
+ * take as input. This data model may eventually become the wire format for metrics.
+ *
+ * <p>WARNING: Currently all the public classes under this package are marked as {@link
+ * io.opencensus.common.ExperimentalApi}. The classes and APIs under {@link io.opencensus.metrics}
+ * are likely to get backwards-incompatible updates in the future. DO NOT USE except for
+ * experimental purposes.
+ *
+ * <p>Please see
+ * https://github.com/census-instrumentation/opencensus-specs/blob/master/stats/Metrics.md and
+ * https://github.com/census-instrumentation/opencensus-proto/blob/master/opencensus/proto/stats/metrics/metrics.proto
+ * for more details.
+ */
+@io.opencensus.common.ExperimentalApi
+package io.opencensus.metrics;
diff --git a/api/src/main/java/io/opencensus/stats/Aggregation.java b/api/src/main/java/io/opencensus/stats/Aggregation.java
new file mode 100644
index 00000000..9c95e847
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/Aggregation.java
@@ -0,0 +1,239 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link Aggregation} is the process of combining a certain set of {@code MeasureValue}s for a
+ * given {@code Measure} into an {@link AggregationData}.
+ *
+ * <p>{@link Aggregation} currently supports 4 types of basic aggregation:
+ *
+ * <ul>
+ * <li>Sum
+ * <li>Count
+ * <li>Distribution
+ * <li>LastValue
+ * </ul>
+ *
+ * <p>When creating a {@link View}, one {@link Aggregation} needs to be specified as how to
+ * aggregate {@code MeasureValue}s.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class Aggregation {
+
+ private Aggregation() {}
+
+ /**
+ * Applies the given match function to the underlying data type.
+ *
+ * @since 0.13
+ */
+ public abstract <T> T match(
+ Function<? super Sum, T> p0,
+ Function<? super Count, T> p1,
+ Function<? super Distribution, T> p2,
+ Function<? super LastValue, T> p3,
+ Function<? super Aggregation, T> defaultFunction);
+
+ /**
+ * Calculate sum on aggregated {@code MeasureValue}s.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class Sum extends Aggregation {
+
+ Sum() {}
+
+ private static final Sum INSTANCE = new AutoValue_Aggregation_Sum();
+
+ /**
+ * Construct a {@code Sum}.
+ *
+ * @return a new {@code Sum}.
+ * @since 0.8
+ */
+ public static Sum create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super Sum, T> p0,
+ Function<? super Count, T> p1,
+ Function<? super Distribution, T> p2,
+ Function<? super LastValue, T> p3,
+ Function<? super Aggregation, T> defaultFunction) {
+ return p0.apply(this);
+ }
+ }
+
+ /**
+ * Calculate count on aggregated {@code MeasureValue}s.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class Count extends Aggregation {
+
+ Count() {}
+
+ private static final Count INSTANCE = new AutoValue_Aggregation_Count();
+
+ /**
+ * Construct a {@code Count}.
+ *
+ * @return a new {@code Count}.
+ * @since 0.8
+ */
+ public static Count create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super Sum, T> p0,
+ Function<? super Count, T> p1,
+ Function<? super Distribution, T> p2,
+ Function<? super LastValue, T> p3,
+ Function<? super Aggregation, T> defaultFunction) {
+ return p1.apply(this);
+ }
+ }
+
+ /**
+ * Calculate mean on aggregated {@code MeasureValue}s.
+ *
+ * @since 0.8
+ * @deprecated since 0.13, use {@link Distribution} instead.
+ */
+ @Immutable
+ @AutoValue
+ @Deprecated
+ @AutoValue.CopyAnnotations
+ public abstract static class Mean extends Aggregation {
+
+ Mean() {}
+
+ private static final Mean INSTANCE = new AutoValue_Aggregation_Mean();
+
+ /**
+ * Construct a {@code Mean}.
+ *
+ * @return a new {@code Mean}.
+ * @since 0.8
+ */
+ public static Mean create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super Sum, T> p0,
+ Function<? super Count, T> p1,
+ Function<? super Distribution, T> p2,
+ Function<? super LastValue, T> p3,
+ Function<? super Aggregation, T> defaultFunction) {
+ return defaultFunction.apply(this);
+ }
+ }
+
+ /**
+ * Calculate distribution stats on aggregated {@code MeasureValue}s. Distribution includes mean,
+ * count, histogram, min, max and sum of squared deviations.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class Distribution extends Aggregation {
+
+ Distribution() {}
+
+ /**
+ * Construct a {@code Distribution}.
+ *
+ * @return a new {@code Distribution}.
+ * @since 0.8
+ */
+ public static Distribution create(BucketBoundaries bucketBoundaries) {
+ Utils.checkNotNull(bucketBoundaries, "bucketBoundaries");
+ return new AutoValue_Aggregation_Distribution(bucketBoundaries);
+ }
+
+ /**
+ * Returns the {@code Distribution}'s bucket boundaries.
+ *
+ * @return the {@code Distribution}'s bucket boundaries.
+ * @since 0.8
+ */
+ public abstract BucketBoundaries getBucketBoundaries();
+
+ @Override
+ public final <T> T match(
+ Function<? super Sum, T> p0,
+ Function<? super Count, T> p1,
+ Function<? super Distribution, T> p2,
+ Function<? super LastValue, T> p3,
+ Function<? super Aggregation, T> defaultFunction) {
+ return p2.apply(this);
+ }
+ }
+
+ /**
+ * Calculate the last value of aggregated {@code MeasureValue}s.
+ *
+ * @since 0.13
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class LastValue extends Aggregation {
+
+ LastValue() {}
+
+ private static final LastValue INSTANCE = new AutoValue_Aggregation_LastValue();
+
+ /**
+ * Construct a {@code LastValue}.
+ *
+ * @return a new {@code LastValue}.
+ * @since 0.13
+ */
+ public static LastValue create() {
+ return INSTANCE;
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super Sum, T> p0,
+ Function<? super Count, T> p1,
+ Function<? super Distribution, T> p2,
+ Function<? super LastValue, T> p3,
+ Function<? super Aggregation, T> defaultFunction) {
+ return p3.apply(this);
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/stats/AggregationData.java b/api/src/main/java/io/opencensus/stats/AggregationData.java
new file mode 100644
index 00000000..c6e12b67
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/AggregationData.java
@@ -0,0 +1,555 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link AggregationData} is the result of applying a given {@link Aggregation} to a set of {@code
+ * MeasureValue}s.
+ *
+ * <p>{@link AggregationData} currently supports 6 types of basic aggregation values:
+ *
+ * <ul>
+ * <li>SumDataDouble
+ * <li>SumDataLong
+ * <li>CountData
+ * <li>DistributionData
+ * <li>LastValueDataDouble
+ * <li>LastValueDataLong
+ * </ul>
+ *
+ * <p>{@link ViewData} will contain one {@link AggregationData}, corresponding to its {@link
+ * Aggregation} definition in {@link View}.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class AggregationData {
+
+ private AggregationData() {}
+
+ /**
+ * Applies the given match function to the underlying data type.
+ *
+ * @since 0.13
+ */
+ public abstract <T> T match(
+ Function<? super SumDataDouble, T> p0,
+ Function<? super SumDataLong, T> p1,
+ Function<? super CountData, T> p2,
+ Function<? super DistributionData, T> p3,
+ Function<? super LastValueDataDouble, T> p4,
+ Function<? super LastValueDataLong, T> p5,
+ Function<? super AggregationData, T> defaultFunction);
+
+ /**
+ * The sum value of aggregated {@code MeasureValueDouble}s.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class SumDataDouble extends AggregationData {
+
+ SumDataDouble() {}
+
+ /**
+ * Creates a {@code SumDataDouble}.
+ *
+ * @param sum the aggregated sum.
+ * @return a {@code SumDataDouble}.
+ * @since 0.8
+ */
+ public static SumDataDouble create(double sum) {
+ return new AutoValue_AggregationData_SumDataDouble(sum);
+ }
+
+ /**
+ * Returns the aggregated sum.
+ *
+ * @return the aggregated sum.
+ * @since 0.8
+ */
+ public abstract double getSum();
+
+ @Override
+ public final <T> T match(
+ Function<? super SumDataDouble, T> p0,
+ Function<? super SumDataLong, T> p1,
+ Function<? super CountData, T> p2,
+ Function<? super DistributionData, T> p3,
+ Function<? super LastValueDataDouble, T> p4,
+ Function<? super LastValueDataLong, T> p5,
+ Function<? super AggregationData, T> defaultFunction) {
+ return p0.apply(this);
+ }
+ }
+
+ /**
+ * The sum value of aggregated {@code MeasureValueLong}s.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class SumDataLong extends AggregationData {
+
+ SumDataLong() {}
+
+ /**
+ * Creates a {@code SumDataLong}.
+ *
+ * @param sum the aggregated sum.
+ * @return a {@code SumDataLong}.
+ * @since 0.8
+ */
+ public static SumDataLong create(long sum) {
+ return new AutoValue_AggregationData_SumDataLong(sum);
+ }
+
+ /**
+ * Returns the aggregated sum.
+ *
+ * @return the aggregated sum.
+ * @since 0.8
+ */
+ public abstract long getSum();
+
+ @Override
+ public final <T> T match(
+ Function<? super SumDataDouble, T> p0,
+ Function<? super SumDataLong, T> p1,
+ Function<? super CountData, T> p2,
+ Function<? super DistributionData, T> p3,
+ Function<? super LastValueDataDouble, T> p4,
+ Function<? super LastValueDataLong, T> p5,
+ Function<? super AggregationData, T> defaultFunction) {
+ return p1.apply(this);
+ }
+ }
+
+ /**
+ * The count value of aggregated {@code MeasureValue}s.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class CountData extends AggregationData {
+
+ CountData() {}
+
+ /**
+ * Creates a {@code CountData}.
+ *
+ * @param count the aggregated count.
+ * @return a {@code CountData}.
+ * @since 0.8
+ */
+ public static CountData create(long count) {
+ return new AutoValue_AggregationData_CountData(count);
+ }
+
+ /**
+ * Returns the aggregated count.
+ *
+ * @return the aggregated count.
+ * @since 0.8
+ */
+ public abstract long getCount();
+
+ @Override
+ public final <T> T match(
+ Function<? super SumDataDouble, T> p0,
+ Function<? super SumDataLong, T> p1,
+ Function<? super CountData, T> p2,
+ Function<? super DistributionData, T> p3,
+ Function<? super LastValueDataDouble, T> p4,
+ Function<? super LastValueDataLong, T> p5,
+ Function<? super AggregationData, T> defaultFunction) {
+ return p2.apply(this);
+ }
+ }
+
+ /**
+ * The mean value of aggregated {@code MeasureValue}s.
+ *
+ * @since 0.8
+ * @deprecated since 0.13, use {@link DistributionData} instead.
+ */
+ @Immutable
+ @AutoValue
+ @Deprecated
+ @AutoValue.CopyAnnotations
+ public abstract static class MeanData extends AggregationData {
+
+ MeanData() {}
+
+ /**
+ * Creates a {@code MeanData}.
+ *
+ * @param mean the aggregated mean.
+ * @param count the aggregated count.
+ * @return a {@code MeanData}.
+ * @since 0.8
+ */
+ public static MeanData create(double mean, long count) {
+ return new AutoValue_AggregationData_MeanData(mean, count);
+ }
+
+ /**
+ * Returns the aggregated mean.
+ *
+ * @return the aggregated mean.
+ * @since 0.8
+ */
+ public abstract double getMean();
+
+ /**
+ * Returns the aggregated count.
+ *
+ * @return the aggregated count.
+ * @since 0.8
+ */
+ public abstract long getCount();
+
+ @Override
+ public final <T> T match(
+ Function<? super SumDataDouble, T> p0,
+ Function<? super SumDataLong, T> p1,
+ Function<? super CountData, T> p2,
+ Function<? super DistributionData, T> p3,
+ Function<? super LastValueDataDouble, T> p4,
+ Function<? super LastValueDataLong, T> p5,
+ Function<? super AggregationData, T> defaultFunction) {
+ return defaultFunction.apply(this);
+ }
+ }
+
+ /**
+ * The distribution stats of aggregated {@code MeasureValue}s. Distribution stats include mean,
+ * count, histogram, min, max and sum of squared deviations.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class DistributionData extends AggregationData {
+
+ DistributionData() {}
+
+ /**
+ * Creates a {@code DistributionData}.
+ *
+ * @param mean mean value.
+ * @param count count value.
+ * @param min min value.
+ * @param max max value.
+ * @param sumOfSquaredDeviations sum of squared deviations.
+ * @param bucketCounts histogram bucket counts.
+ * @param exemplars the exemplars associated with histogram buckets.
+ * @return a {@code DistributionData}.
+ * @since 0.16
+ */
+ public static DistributionData create(
+ double mean,
+ long count,
+ double min,
+ double max,
+ double sumOfSquaredDeviations,
+ List<Long> bucketCounts,
+ List<Exemplar> exemplars) {
+ if (min != Double.POSITIVE_INFINITY || max != Double.NEGATIVE_INFINITY) {
+ Utils.checkArgument(min <= max, "max should be greater or equal to min.");
+ }
+
+ Utils.checkNotNull(bucketCounts, "bucketCounts");
+ List<Long> bucketCountsCopy = Collections.unmodifiableList(new ArrayList<Long>(bucketCounts));
+ for (Long bucket : bucketCountsCopy) {
+ Utils.checkNotNull(bucket, "bucket");
+ }
+
+ Utils.checkNotNull(exemplars, "exemplar list should not be null.");
+ for (Exemplar exemplar : exemplars) {
+ Utils.checkNotNull(exemplar, "exemplar");
+ }
+
+ return new AutoValue_AggregationData_DistributionData(
+ mean,
+ count,
+ min,
+ max,
+ sumOfSquaredDeviations,
+ bucketCountsCopy,
+ Collections.<Exemplar>unmodifiableList(new ArrayList<Exemplar>(exemplars)));
+ }
+
+ /**
+ * Creates a {@code DistributionData}.
+ *
+ * @param mean mean value.
+ * @param count count value.
+ * @param min min value.
+ * @param max max value.
+ * @param sumOfSquaredDeviations sum of squared deviations.
+ * @param bucketCounts histogram bucket counts.
+ * @return a {@code DistributionData}.
+ * @since 0.8
+ */
+ public static DistributionData create(
+ double mean,
+ long count,
+ double min,
+ double max,
+ double sumOfSquaredDeviations,
+ List<Long> bucketCounts) {
+ return create(
+ mean,
+ count,
+ min,
+ max,
+ sumOfSquaredDeviations,
+ bucketCounts,
+ Collections.<Exemplar>emptyList());
+ }
+
+ /**
+ * Returns the aggregated mean.
+ *
+ * @return the aggregated mean.
+ * @since 0.8
+ */
+ public abstract double getMean();
+
+ /**
+ * Returns the aggregated count.
+ *
+ * @return the aggregated count.
+ * @since 0.8
+ */
+ public abstract long getCount();
+
+ /**
+ * Returns the minimum of the population values.
+ *
+ * @return the minimum of the population values.
+ * @since 0.8
+ */
+ public abstract double getMin();
+
+ /**
+ * Returns the maximum of the population values.
+ *
+ * @return the maximum of the population values.
+ * @since 0.8
+ */
+ public abstract double getMax();
+
+ /**
+ * Returns the aggregated sum of squared deviations.
+ *
+ * @return the aggregated sum of squared deviations.
+ * @since 0.8
+ */
+ public abstract double getSumOfSquaredDeviations();
+
+ /**
+ * Returns the aggregated bucket counts. The returned list is immutable, trying to update it
+ * will throw an {@code UnsupportedOperationException}.
+ *
+ * @return the aggregated bucket counts.
+ * @since 0.8
+ */
+ public abstract List<Long> getBucketCounts();
+
+ /**
+ * Returns the {@link Exemplar}s associated with histogram buckets.
+ *
+ * @return the {@code Exemplar}s associated with histogram buckets.
+ * @since 0.16
+ */
+ public abstract List<Exemplar> getExemplars();
+
+ @Override
+ public final <T> T match(
+ Function<? super SumDataDouble, T> p0,
+ Function<? super SumDataLong, T> p1,
+ Function<? super CountData, T> p2,
+ Function<? super DistributionData, T> p3,
+ Function<? super LastValueDataDouble, T> p4,
+ Function<? super LastValueDataLong, T> p5,
+ Function<? super AggregationData, T> defaultFunction) {
+ return p3.apply(this);
+ }
+
+ /**
+ * An example point that may be used to annotate aggregated distribution values, associated with
+ * a histogram bucket.
+ *
+ * @since 0.16
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class Exemplar {
+
+ Exemplar() {}
+
+ /**
+ * Returns value of the {@link Exemplar} point.
+ *
+ * @return value of the {@code Exemplar} point.
+ * @since 0.16
+ */
+ public abstract double getValue();
+
+ /**
+ * Returns the time that this {@link Exemplar}'s value was recorded.
+ *
+ * @return the time that this {@code Exemplar}'s value was recorded.
+ * @since 0.16
+ */
+ public abstract Timestamp getTimestamp();
+
+ /**
+ * Returns the contextual information about the example value, represented as a string map.
+ *
+ * @return the contextual information about the example value.
+ * @since 0.16
+ */
+ public abstract Map<String, String> getAttachments();
+
+ /**
+ * Creates an {@link Exemplar}.
+ *
+ * @param value value of the {@link Exemplar} point.
+ * @param timestamp the time that this {@code Exemplar}'s value was recorded.
+ * @param attachments the contextual information about the example value.
+ * @return an {@code Exemplar}.
+ * @since 0.16
+ */
+ public static Exemplar create(
+ double value, Timestamp timestamp, Map<String, String> attachments) {
+ Utils.checkNotNull(attachments, "attachments");
+ Map<String, String> attachmentsCopy =
+ Collections.unmodifiableMap(new HashMap<String, String>(attachments));
+ for (Entry<String, String> entry : attachmentsCopy.entrySet()) {
+ Utils.checkNotNull(entry.getKey(), "key of attachments");
+ Utils.checkNotNull(entry.getValue(), "value of attachments");
+ }
+ return new AutoValue_AggregationData_DistributionData_Exemplar(
+ value, timestamp, attachmentsCopy);
+ }
+ }
+ }
+
+ /**
+ * The last value of aggregated {@code MeasureValueDouble}s.
+ *
+ * @since 0.13
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class LastValueDataDouble extends AggregationData {
+
+ LastValueDataDouble() {}
+
+ /**
+ * Creates a {@code LastValueDataDouble}.
+ *
+ * @param lastValue the last value.
+ * @return a {@code LastValueDataDouble}.
+ * @since 0.13
+ */
+ public static LastValueDataDouble create(double lastValue) {
+ return new AutoValue_AggregationData_LastValueDataDouble(lastValue);
+ }
+
+ /**
+ * Returns the last value.
+ *
+ * @return the last value.
+ * @since 0.13
+ */
+ public abstract double getLastValue();
+
+ @Override
+ public final <T> T match(
+ Function<? super SumDataDouble, T> p0,
+ Function<? super SumDataLong, T> p1,
+ Function<? super CountData, T> p2,
+ Function<? super DistributionData, T> p3,
+ Function<? super LastValueDataDouble, T> p4,
+ Function<? super LastValueDataLong, T> p5,
+ Function<? super AggregationData, T> defaultFunction) {
+ return p4.apply(this);
+ }
+ }
+
+ /**
+ * The last value of aggregated {@code MeasureValueLong}s.
+ *
+ * @since 0.13
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class LastValueDataLong extends AggregationData {
+
+ LastValueDataLong() {}
+
+ /**
+ * Creates a {@code LastValueDataLong}.
+ *
+ * @param lastValue the last value.
+ * @return a {@code LastValueDataLong}.
+ * @since 0.13
+ */
+ public static LastValueDataLong create(long lastValue) {
+ return new AutoValue_AggregationData_LastValueDataLong(lastValue);
+ }
+
+ /**
+ * Returns the last value.
+ *
+ * @return the last value.
+ * @since 0.13
+ */
+ public abstract long getLastValue();
+
+ @Override
+ public final <T> T match(
+ Function<? super SumDataDouble, T> p0,
+ Function<? super SumDataLong, T> p1,
+ Function<? super CountData, T> p2,
+ Function<? super DistributionData, T> p3,
+ Function<? super LastValueDataDouble, T> p4,
+ Function<? super LastValueDataLong, T> p5,
+ Function<? super AggregationData, T> defaultFunction) {
+ return p5.apply(this);
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/stats/BucketBoundaries.java b/api/src/main/java/io/opencensus/stats/BucketBoundaries.java
new file mode 100644
index 00000000..61e21e6c
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/BucketBoundaries.java
@@ -0,0 +1,66 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The bucket boundaries for a histogram.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+public abstract class BucketBoundaries {
+
+ /**
+ * Returns a {@code BucketBoundaries} with the given buckets.
+ *
+ * @param bucketBoundaries the boundaries for the buckets in the underlying histogram.
+ * @return a new {@code BucketBoundaries} with the specified boundaries.
+ * @throws NullPointerException if {@code bucketBoundaries} is null.
+ * @throws IllegalArgumentException if {@code bucketBoundaries} is not sorted.
+ * @since 0.8
+ */
+ public static final BucketBoundaries create(List<Double> bucketBoundaries) {
+ Utils.checkNotNull(bucketBoundaries, "bucketBoundaries");
+ List<Double> bucketBoundariesCopy = new ArrayList<Double>(bucketBoundaries); // Deep copy.
+ // Check if sorted.
+ if (bucketBoundariesCopy.size() > 1) {
+ double lower = bucketBoundariesCopy.get(0);
+ for (int i = 1; i < bucketBoundariesCopy.size(); i++) {
+ double next = bucketBoundariesCopy.get(i);
+ Utils.checkArgument(lower < next, "Bucket boundaries not sorted.");
+ lower = next;
+ }
+ }
+ return new AutoValue_BucketBoundaries(Collections.unmodifiableList(bucketBoundariesCopy));
+ }
+
+ /**
+ * Returns a list of histogram bucket boundaries.
+ *
+ * @return a list of histogram bucket boundaries.
+ * @since 0.8
+ */
+ public abstract List<Double> getBoundaries();
+}
diff --git a/api/src/main/java/io/opencensus/stats/Measure.java b/api/src/main/java/io/opencensus/stats/Measure.java
new file mode 100644
index 00000000..2de7fd70
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/Measure.java
@@ -0,0 +1,177 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.StringUtils;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The definition of the {@link Measurement} that is taken by OpenCensus library.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class Measure {
+ @DefaultVisibilityForTesting static final int NAME_MAX_LENGTH = 255;
+ private static final String ERROR_MESSAGE_INVALID_NAME =
+ "Name should be a ASCII string with a length no greater than "
+ + NAME_MAX_LENGTH
+ + " characters.";
+
+ /**
+ * Applies the given match function to the underlying data type.
+ *
+ * @since 0.8
+ */
+ public abstract <T> T match(
+ Function<? super MeasureDouble, T> p0,
+ Function<? super MeasureLong, T> p1,
+ Function<? super Measure, T> defaultFunction);
+
+ /**
+ * Name of measure, as a {@code String}. Should be a ASCII string with a length no greater than
+ * 255 characters.
+ *
+ * <p>Suggested format for name: {@code <web_host>/<path>}.
+ *
+ * @since 0.8
+ */
+ public abstract String getName();
+
+ /**
+ * Detailed description of the measure, used in documentation.
+ *
+ * @since 0.8
+ */
+ public abstract String getDescription();
+
+ /**
+ * The units in which {@link Measure} values are measured.
+ *
+ * <p>The suggested grammar for a unit is as follows:
+ *
+ * <ul>
+ * <li>Expression = Component { "." Component } {"/" Component };
+ * <li>Component = [ PREFIX ] UNIT [ Annotation ] | Annotation | "1";
+ * <li>Annotation = "{" NAME "}" ;
+ * </ul>
+ *
+ * <p>For example, string “MBy{transmitted}/ms” stands for megabytes per milliseconds, and the
+ * annotation transmitted inside {} is just a comment of the unit.
+ *
+ * @since 0.8
+ */
+ // TODO(songya): determine whether we want to check the grammar on string unit.
+ public abstract String getUnit();
+
+ // Prevents this class from being subclassed anywhere else.
+ private Measure() {}
+
+ /**
+ * {@link Measure} with {@code Double} typed values.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class MeasureDouble extends Measure {
+
+ MeasureDouble() {}
+
+ /**
+ * Constructs a new {@link MeasureDouble}.
+ *
+ * @param name name of {@code Measure}. Suggested format: {@code <web_host>/<path>}.
+ * @param description description of {@code Measure}.
+ * @param unit unit of {@code Measure}.
+ * @return a {@code MeasureDouble}.
+ * @since 0.8
+ */
+ public static MeasureDouble create(String name, String description, String unit) {
+ Utils.checkArgument(
+ StringUtils.isPrintableString(name) && name.length() <= NAME_MAX_LENGTH,
+ ERROR_MESSAGE_INVALID_NAME);
+ return new AutoValue_Measure_MeasureDouble(name, description, unit);
+ }
+
+ @Override
+ public <T> T match(
+ Function<? super MeasureDouble, T> p0,
+ Function<? super MeasureLong, T> p1,
+ Function<? super Measure, T> defaultFunction) {
+ return p0.apply(this);
+ }
+
+ @Override
+ public abstract String getName();
+
+ @Override
+ public abstract String getDescription();
+
+ @Override
+ public abstract String getUnit();
+ }
+
+ /**
+ * {@link Measure} with {@code Long} typed values.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class MeasureLong extends Measure {
+
+ MeasureLong() {}
+
+ /**
+ * Constructs a new {@link MeasureLong}.
+ *
+ * @param name name of {@code Measure}. Suggested format: {@code <web_host>/<path>}.
+ * @param description description of {@code Measure}.
+ * @param unit unit of {@code Measure}.
+ * @return a {@code MeasureLong}.
+ * @since 0.8
+ */
+ public static MeasureLong create(String name, String description, String unit) {
+ Utils.checkArgument(
+ StringUtils.isPrintableString(name) && name.length() <= NAME_MAX_LENGTH,
+ ERROR_MESSAGE_INVALID_NAME);
+ return new AutoValue_Measure_MeasureLong(name, description, unit);
+ }
+
+ @Override
+ public <T> T match(
+ Function<? super MeasureDouble, T> p0,
+ Function<? super MeasureLong, T> p1,
+ Function<? super Measure, T> defaultFunction) {
+ return p1.apply(this);
+ }
+
+ @Override
+ public abstract String getName();
+
+ @Override
+ public abstract String getDescription();
+
+ @Override
+ public abstract String getUnit();
+ }
+}
diff --git a/api/src/main/java/io/opencensus/stats/MeasureMap.java b/api/src/main/java/io/opencensus/stats/MeasureMap.java
new file mode 100644
index 00000000..beb84f06
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/MeasureMap.java
@@ -0,0 +1,92 @@
+/*
+ * 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.stats;
+
+import io.opencensus.internal.Utils;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagContext;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * A map from {@link Measure}s to measured values to be recorded at the same time.
+ *
+ * @since 0.8
+ */
+@NotThreadSafe
+public abstract class MeasureMap {
+
+ /**
+ * 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
+ * @since 0.8
+ */
+ public abstract MeasureMap put(MeasureDouble measure, double value);
+
+ /**
+ * 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
+ * @since 0.8
+ */
+ public abstract MeasureMap put(MeasureLong measure, long value);
+
+ /**
+ * Associate the contextual information of an {@code Exemplar} to this {@link MeasureMap}.
+ * Contextual information is represented as {@code String} key-value pairs.
+ *
+ * <p>If this method is called multiple times with the same key, only the last value will be kept.
+ *
+ * @param key the key of contextual information of an {@code Exemplar}.
+ * @param value the string representation of contextual information of an {@code Exemplar}.
+ * @return this
+ * @since 0.16
+ */
+ // TODO(songya): make this method abstract in the 0.17 release.
+ public MeasureMap putAttachment(String key, String value) {
+ // Provides a default no-op implementation to avoid breaking other existing sub-classes.
+ Utils.checkNotNull(key, "key");
+ Utils.checkNotNull(value, "value");
+ return this;
+ }
+
+ /**
+ * Records all of the measures at the same time, with the current {@link TagContext}.
+ *
+ * <p>This method records all of the stats in the {@code MeasureMap} every time it is called.
+ *
+ * @since 0.8
+ */
+ public abstract void record();
+
+ /**
+ * Records all of the measures at the same time, with an explicit {@link TagContext}.
+ *
+ * <p>This method records all of the stats in the {@code MeasureMap} every time it is called.
+ *
+ * @param tags the tags associated with the measurements.
+ * @since 0.8
+ */
+ public abstract void record(TagContext tags);
+}
diff --git a/api/src/main/java/io/opencensus/stats/Measurement.java b/api/src/main/java/io/opencensus/stats/Measurement.java
new file mode 100644
index 00000000..647a667d
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/Measurement.java
@@ -0,0 +1,130 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Immutable representation of a Measurement.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class Measurement {
+
+ /**
+ * Applies the given match function to the underlying data type.
+ *
+ * @since 0.8
+ */
+ public abstract <T> T match(
+ Function<? super MeasurementDouble, T> p0,
+ Function<? super MeasurementLong, T> p1,
+ Function<? super Measurement, T> defaultFunction);
+
+ /**
+ * Extracts the measured {@link Measure}.
+ *
+ * @since 0.8
+ */
+ public abstract Measure getMeasure();
+
+ // Prevents this class from being subclassed anywhere else.
+ private Measurement() {}
+
+ /**
+ * {@code Double} typed {@link Measurement}.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class MeasurementDouble extends Measurement {
+ MeasurementDouble() {}
+
+ /**
+ * Constructs a new {@link MeasurementDouble}.
+ *
+ * @since 0.8
+ */
+ public static MeasurementDouble create(MeasureDouble measure, double value) {
+ return new AutoValue_Measurement_MeasurementDouble(measure, value);
+ }
+
+ @Override
+ public abstract MeasureDouble getMeasure();
+
+ /**
+ * Returns the value for the measure.
+ *
+ * @return the value for the measure.
+ * @since 0.8
+ */
+ public abstract double getValue();
+
+ @Override
+ public <T> T match(
+ Function<? super MeasurementDouble, T> p0,
+ Function<? super MeasurementLong, T> p1,
+ Function<? super Measurement, T> defaultFunction) {
+ return p0.apply(this);
+ }
+ }
+
+ /**
+ * {@code Long} typed {@link Measurement}.
+ *
+ * @since 0.8
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class MeasurementLong extends Measurement {
+ MeasurementLong() {}
+
+ /**
+ * Constructs a new {@link MeasurementLong}.
+ *
+ * @since 0.8
+ */
+ public static MeasurementLong create(MeasureLong measure, long value) {
+ return new AutoValue_Measurement_MeasurementLong(measure, value);
+ }
+
+ @Override
+ public abstract MeasureLong getMeasure();
+
+ /**
+ * Returns the value for the measure.
+ *
+ * @return the value for the measure.
+ * @since 0.8
+ */
+ public abstract long getValue();
+
+ @Override
+ public <T> T match(
+ Function<? super MeasurementDouble, T> p0,
+ Function<? super MeasurementLong, T> p1,
+ Function<? super Measurement, T> defaultFunction) {
+ return p1.apply(this);
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/stats/NoopStats.java b/api/src/main/java/io/opencensus/stats/NoopStats.java
new file mode 100644
index 00000000..e7e94a38
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/NoopStats.java
@@ -0,0 +1,221 @@
+/*
+ * 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.stats;
+
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagValue;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** No-op implementations of stats classes. */
+final class NoopStats {
+
+ private NoopStats() {}
+
+ /**
+ * Returns a {@code StatsComponent} that has a no-op implementation for {@link StatsRecorder}.
+ *
+ * @return a {@code StatsComponent} that has a no-op implementation for {@code StatsRecorder}.
+ */
+ static StatsComponent newNoopStatsComponent() {
+ return new NoopStatsComponent();
+ }
+
+ /**
+ * Returns a {@code StatsRecorder} that does not record any data.
+ *
+ * @return a {@code StatsRecorder} that does not record any data.
+ */
+ static StatsRecorder getNoopStatsRecorder() {
+ return NoopStatsRecorder.INSTANCE;
+ }
+
+ /**
+ * Returns a {@code MeasureMap} that ignores all calls to {@link MeasureMap#put}.
+ *
+ * @return a {@code MeasureMap} that ignores all calls to {@code MeasureMap#put}.
+ */
+ static MeasureMap getNoopMeasureMap() {
+ return NoopMeasureMap.INSTANCE;
+ }
+
+ /**
+ * Returns a {@code ViewManager} that maintains a map of views, but always returns empty {@link
+ * ViewData}s.
+ *
+ * @return a {@code ViewManager} that maintains a map of views, but always returns empty {@code
+ * ViewData}s.
+ */
+ static ViewManager newNoopViewManager() {
+ return new NoopViewManager();
+ }
+
+ @ThreadSafe
+ private static final class NoopStatsComponent extends StatsComponent {
+ private final ViewManager viewManager = newNoopViewManager();
+ private volatile boolean isRead;
+
+ @Override
+ public ViewManager getViewManager() {
+ return viewManager;
+ }
+
+ @Override
+ public StatsRecorder getStatsRecorder() {
+ return getNoopStatsRecorder();
+ }
+
+ @Override
+ public StatsCollectionState getState() {
+ isRead = true;
+ return StatsCollectionState.DISABLED;
+ }
+
+ @Override
+ @Deprecated
+ public void setState(StatsCollectionState state) {
+ Utils.checkNotNull(state, "state");
+ Utils.checkState(!isRead, "State was already read, cannot set state.");
+ }
+ }
+
+ @Immutable
+ private static final class NoopStatsRecorder extends StatsRecorder {
+ static final StatsRecorder INSTANCE = new NoopStatsRecorder();
+
+ @Override
+ public MeasureMap newMeasureMap() {
+ return getNoopMeasureMap();
+ }
+ }
+
+ @Immutable
+ private static final class NoopMeasureMap extends MeasureMap {
+ static final MeasureMap INSTANCE = new NoopMeasureMap();
+
+ @Override
+ public MeasureMap put(MeasureDouble measure, double value) {
+ return this;
+ }
+
+ @Override
+ public MeasureMap put(MeasureLong measure, long value) {
+ return this;
+ }
+
+ @Override
+ public void record() {}
+
+ @Override
+ public void record(TagContext tags) {
+ Utils.checkNotNull(tags, "tags");
+ }
+ }
+
+ @ThreadSafe
+ private static final class NoopViewManager extends ViewManager {
+ private static final Timestamp ZERO_TIMESTAMP = Timestamp.create(0, 0);
+
+ @GuardedBy("registeredViews")
+ private final Map<View.Name, View> registeredViews = new HashMap<View.Name, View>();
+
+ // Cached set of exported views. It must be set to null whenever a view is registered or
+ // unregistered.
+ @javax.annotation.Nullable private volatile Set<View> exportedViews;
+
+ @Override
+ public void registerView(View newView) {
+ Utils.checkNotNull(newView, "newView");
+ synchronized (registeredViews) {
+ exportedViews = null;
+ View existing = registeredViews.get(newView.getName());
+ Utils.checkArgument(
+ existing == null || newView.equals(existing),
+ "A different view with the same name already exists.");
+ if (existing == null) {
+ registeredViews.put(newView.getName(), newView);
+ }
+ }
+ }
+
+ @Override
+ @javax.annotation.Nullable
+ @SuppressWarnings("deprecation")
+ public ViewData getView(View.Name name) {
+ Utils.checkNotNull(name, "name");
+ synchronized (registeredViews) {
+ View view = registeredViews.get(name);
+ if (view == null) {
+ return null;
+ } else {
+ return ViewData.create(
+ view,
+ Collections.<List</*@Nullable*/ TagValue>, AggregationData>emptyMap(),
+ view.getWindow()
+ .match(
+ Functions.<ViewData.AggregationWindowData>returnConstant(
+ ViewData.AggregationWindowData.CumulativeData.create(
+ ZERO_TIMESTAMP, ZERO_TIMESTAMP)),
+ Functions.<ViewData.AggregationWindowData>returnConstant(
+ ViewData.AggregationWindowData.IntervalData.create(ZERO_TIMESTAMP)),
+ Functions.<ViewData.AggregationWindowData>throwAssertionError()));
+ }
+ }
+ }
+
+ @Override
+ public Set<View> getAllExportedViews() {
+ Set<View> views = exportedViews;
+ if (views == null) {
+ synchronized (registeredViews) {
+ exportedViews = views = filterExportedViews(registeredViews.values());
+ }
+ }
+ return views;
+ }
+
+ // Returns the subset of the given views that should be exported
+ @SuppressWarnings("deprecation")
+ private static Set<View> filterExportedViews(Collection<View> allViews) {
+ Set<View> views = new HashSet<View>();
+ for (View view : allViews) {
+ if (view.getWindow() instanceof View.AggregationWindow.Interval) {
+ continue;
+ }
+ views.add(view);
+ }
+ return Collections.unmodifiableSet(views);
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/stats/Stats.java b/api/src/main/java/io/opencensus/stats/Stats.java
new file mode 100644
index 00000000..8393f631
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/Stats.java
@@ -0,0 +1,126 @@
+/*
+ * 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.stats;
+
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Provider;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Class for accessing the default {@link StatsComponent}.
+ *
+ * @since 0.8
+ */
+public final class Stats {
+ private static final Logger logger = Logger.getLogger(Stats.class.getName());
+
+ private static final StatsComponent statsComponent =
+ loadStatsComponent(StatsComponent.class.getClassLoader());
+
+ /**
+ * Returns the default {@link StatsRecorder}.
+ *
+ * @since 0.8
+ */
+ public static StatsRecorder getStatsRecorder() {
+ return statsComponent.getStatsRecorder();
+ }
+
+ /**
+ * Returns the default {@link ViewManager}.
+ *
+ * @since 0.8
+ */
+ public static ViewManager getViewManager() {
+ return statsComponent.getViewManager();
+ }
+
+ /**
+ * Returns the current {@code StatsCollectionState}.
+ *
+ * <p>When no implementation is available, {@code getState} always returns {@link
+ * StatsCollectionState#DISABLED}.
+ *
+ * <p>Once {@link #getState()} is called, subsequent calls to {@link
+ * #setState(StatsCollectionState)} will throw an {@code IllegalStateException}.
+ *
+ * @return the current {@code StatsCollectionState}.
+ * @since 0.8
+ */
+ public static StatsCollectionState getState() {
+ return statsComponent.getState();
+ }
+
+ /**
+ * Sets the current {@code StatsCollectionState}.
+ *
+ * <p>When no implementation is available, {@code setState} does not change the state.
+ *
+ * <p>If state is set to {@link StatsCollectionState#DISABLED}, all stats that are previously
+ * recorded will be cleared.
+ *
+ * @param state the new {@code StatsCollectionState}.
+ * @throws IllegalStateException if {@link #getState()} was previously called.
+ * @deprecated This method is deprecated because other libraries could cache the result of {@link
+ * #getState()}, use a stale value, and behave incorrectly. It is only safe to call early in
+ * initialization. This method throws {@link IllegalStateException} after {@code getState()}
+ * has been called, in order to limit changes to the result of {@code getState()}.
+ * @since 0.8
+ */
+ @Deprecated
+ public static void setState(StatsCollectionState state) {
+ statsComponent.setState(state);
+ }
+
+ // Any provider that may be used for StatsComponent can be added here.
+ @DefaultVisibilityForTesting
+ static StatsComponent loadStatsComponent(@Nullable ClassLoader classLoader) {
+ try {
+ // Call Class.forName with literal string name of the class to help shading tools.
+ return Provider.createInstance(
+ Class.forName(
+ "io.opencensus.impl.stats.StatsComponentImpl", /*initialize=*/ true, classLoader),
+ StatsComponent.class);
+ } catch (ClassNotFoundException e) {
+ logger.log(
+ Level.FINE,
+ "Couldn't load full implementation for StatsComponent, now trying to load lite "
+ + "implementation.",
+ e);
+ }
+ try {
+ // Call Class.forName with literal string name of the class to help shading tools.
+ return Provider.createInstance(
+ Class.forName(
+ "io.opencensus.impllite.stats.StatsComponentImplLite",
+ /*initialize=*/ true,
+ classLoader),
+ StatsComponent.class);
+ } catch (ClassNotFoundException e) {
+ logger.log(
+ Level.FINE,
+ "Couldn't load lite implementation for StatsComponent, now using "
+ + "default implementation for StatsComponent.",
+ e);
+ }
+ return NoopStats.newNoopStatsComponent();
+ }
+
+ private Stats() {}
+}
diff --git a/api/src/main/java/io/opencensus/stats/StatsCollectionState.java b/api/src/main/java/io/opencensus/stats/StatsCollectionState.java
new file mode 100644
index 00000000..6b2f2409
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/StatsCollectionState.java
@@ -0,0 +1,44 @@
+/*
+ * 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.stats;
+
+/**
+ * State of the {@link StatsComponent}.
+ *
+ * @since 0.8
+ */
+public enum StatsCollectionState {
+
+ /**
+ * State that fully enables stats collection.
+ *
+ * <p>The {@link StatsComponent} collects stats for registered views.
+ *
+ * @since 0.8
+ */
+ ENABLED,
+
+ /**
+ * State that disables stats collection.
+ *
+ * <p>The {@link StatsComponent} does not need to collect stats for registered views and may
+ * return empty {@link ViewData}s from {@link ViewManager#getView(View.Name)}.
+ *
+ * @since 0.8
+ */
+ DISABLED
+}
diff --git a/api/src/main/java/io/opencensus/stats/StatsComponent.java b/api/src/main/java/io/opencensus/stats/StatsComponent.java
new file mode 100644
index 00000000..9764fce5
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/StatsComponent.java
@@ -0,0 +1,74 @@
+/*
+ * 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.stats;
+
+/**
+ * Class that holds the implementations for {@link ViewManager} and {@link StatsRecorder}.
+ *
+ * <p>All objects returned by methods on {@code StatsComponent} are cacheable.
+ *
+ * @since 0.8
+ */
+public abstract class StatsComponent {
+
+ /**
+ * Returns the default {@link ViewManager}.
+ *
+ * @since 0.8
+ */
+ public abstract ViewManager getViewManager();
+
+ /**
+ * Returns the default {@link StatsRecorder}.
+ *
+ * @since 0.8
+ */
+ public abstract StatsRecorder getStatsRecorder();
+
+ /**
+ * Returns the current {@code StatsCollectionState}.
+ *
+ * <p>When no implementation is available, {@code getState} always returns {@link
+ * StatsCollectionState#DISABLED}.
+ *
+ * <p>Once {@link #getState()} is called, subsequent calls to {@link
+ * #setState(StatsCollectionState)} will throw an {@code IllegalStateException}.
+ *
+ * @return the current {@code StatsCollectionState}.
+ * @since 0.8
+ */
+ public abstract StatsCollectionState getState();
+
+ /**
+ * Sets the current {@code StatsCollectionState}.
+ *
+ * <p>When no implementation is available, {@code setState} does not change the state.
+ *
+ * <p>If state is set to {@link StatsCollectionState#DISABLED}, all stats that are previously
+ * recorded will be cleared.
+ *
+ * @param state the new {@code StatsCollectionState}.
+ * @throws IllegalStateException if {@link #getState()} was previously called.
+ * @deprecated This method is deprecated because other libraries could cache the result of {@link
+ * #getState()}, use a stale value, and behave incorrectly. It is only safe to call early in
+ * initialization. This method throws {@link IllegalStateException} after {@code getState()}
+ * has been called, in order to limit changes to the result of {@code getState()}.
+ * @since 0.8
+ */
+ @Deprecated
+ public abstract void setState(StatsCollectionState state);
+}
diff --git a/api/src/main/java/io/opencensus/stats/StatsRecorder.java b/api/src/main/java/io/opencensus/stats/StatsRecorder.java
new file mode 100644
index 00000000..87b8c8b0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/StatsRecorder.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.stats;
+
+/**
+ * Provides methods to record stats against tags.
+ *
+ * @since 0.8
+ */
+public abstract class StatsRecorder {
+ // TODO(sebright): Should we provide convenience methods for only recording one measure?
+
+ /**
+ * Returns an object for recording multiple measurements.
+ *
+ * @return an object for recording multiple measurements.
+ * @since 0.8
+ */
+ public abstract MeasureMap newMeasureMap();
+}
diff --git a/api/src/main/java/io/opencensus/stats/View.java b/api/src/main/java/io/opencensus/stats/View.java
new file mode 100644
index 00000000..f563ff9a
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/View.java
@@ -0,0 +1,306 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.StringUtils;
+import io.opencensus.internal.Utils;
+import io.opencensus.tags.TagKey;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A View specifies an aggregation and a set of tag keys. The aggregation will be broken down by the
+ * unique set of matching tag values for each measure.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+@AutoValue.CopyAnnotations
+@SuppressWarnings("deprecation")
+public abstract class View {
+
+ @DefaultVisibilityForTesting static final int NAME_MAX_LENGTH = 255;
+
+ private static final Comparator<TagKey> TAG_KEY_COMPARATOR =
+ new Comparator<TagKey>() {
+ @Override
+ public int compare(TagKey key1, TagKey key2) {
+ return key1.getName().compareTo(key2.getName());
+ }
+ };
+
+ View() {}
+
+ /**
+ * Name of view. Must be unique.
+ *
+ * @since 0.8
+ */
+ public abstract Name getName();
+
+ /**
+ * More detailed description, for documentation purposes.
+ *
+ * @since 0.8
+ */
+ public abstract String getDescription();
+
+ /**
+ * Measure type of this view.
+ *
+ * @since 0.8
+ */
+ public abstract Measure getMeasure();
+
+ /**
+ * The {@link Aggregation} associated with this {@link View}.
+ *
+ * @since 0.8
+ */
+ public abstract Aggregation getAggregation();
+
+ /**
+ * Columns (a.k.a Tag Keys) to match with the associated {@link Measure}.
+ *
+ * <p>{@link Measure} will be recorded in a "greedy" way. That is, every view aggregates every
+ * measure. This is similar to doing a GROUPBY on view’s columns. Columns must be unique.
+ *
+ * @since 0.8
+ */
+ public abstract List<TagKey> getColumns();
+
+ /**
+ * Returns the time {@link AggregationWindow} for this {@code View}.
+ *
+ * @return the time {@link AggregationWindow}.
+ * @since 0.8
+ * @deprecated since 0.13. In the future all {@link View}s will be cumulative.
+ */
+ @Deprecated
+ public abstract AggregationWindow getWindow();
+
+ /**
+ * Constructs a new {@link View}.
+ *
+ * @param name the {@link Name} of view. Must be unique.
+ * @param description the description of view.
+ * @param measure the {@link Measure} to be aggregated by this view.
+ * @param aggregation the basic {@link Aggregation} that this view will support.
+ * @param columns the {@link TagKey}s that this view will aggregate on. Columns should not contain
+ * duplicates.
+ * @param window the {@link AggregationWindow} of view.
+ * @return a new {@link View}.
+ * @since 0.8
+ * @deprecated in favor of {@link #create(Name, String, Measure, Aggregation, List)}.
+ */
+ @Deprecated
+ public static View create(
+ Name name,
+ String description,
+ Measure measure,
+ Aggregation aggregation,
+ List<TagKey> columns,
+ AggregationWindow window) {
+ Utils.checkArgument(
+ new HashSet<TagKey>(columns).size() == columns.size(), "Columns have duplicate.");
+
+ List<TagKey> tagKeys = new ArrayList<TagKey>(columns);
+ Collections.sort(tagKeys, TAG_KEY_COMPARATOR);
+ return new AutoValue_View(
+ name, description, measure, aggregation, Collections.unmodifiableList(tagKeys), window);
+ }
+
+ /**
+ * Constructs a new {@link View}.
+ *
+ * @param name the {@link Name} of view. Must be unique.
+ * @param description the description of view.
+ * @param measure the {@link Measure} to be aggregated by this view.
+ * @param aggregation the basic {@link Aggregation} that this view will support.
+ * @param columns the {@link TagKey}s that this view will aggregate on. Columns should not contain
+ * duplicates.
+ * @return a new {@link View}.
+ * @since 0.13
+ */
+ public static View create(
+ Name name,
+ String description,
+ Measure measure,
+ Aggregation aggregation,
+ List<TagKey> columns) {
+ Utils.checkArgument(
+ new HashSet<TagKey>(columns).size() == columns.size(), "Columns have duplicate.");
+ return create(
+ name, description, measure, aggregation, columns, AggregationWindow.Cumulative.create());
+ }
+
+ /**
+ * The name of a {@code View}.
+ *
+ * @since 0.8
+ */
+ // This type should be used as the key when associating data with Views.
+ @Immutable
+ @AutoValue
+ public abstract static class Name {
+
+ Name() {}
+
+ /**
+ * Returns the name as a {@code String}.
+ *
+ * @return the name as a {@code String}.
+ * @since 0.8
+ */
+ public abstract String asString();
+
+ /**
+ * Creates a {@code View.Name} from a {@code String}. Should be a ASCII string with a length no
+ * greater than 255 characters.
+ *
+ * <p>Suggested format for name: {@code <web_host>/<path>}.
+ *
+ * @param name the name {@code String}.
+ * @return a {@code View.Name} with the given name {@code String}.
+ * @since 0.8
+ */
+ public static Name create(String name) {
+ Utils.checkArgument(
+ StringUtils.isPrintableString(name) && name.length() <= NAME_MAX_LENGTH,
+ "Name should be a ASCII string with a length no greater than 255 characters.");
+ return new AutoValue_View_Name(name);
+ }
+ }
+
+ /**
+ * The time window for a {@code View}.
+ *
+ * @since 0.8
+ * @deprecated since 0.13. In the future all {@link View}s will be cumulative.
+ */
+ @Deprecated
+ @Immutable
+ public abstract static class AggregationWindow {
+
+ private AggregationWindow() {}
+
+ /**
+ * Applies the given match function to the underlying data type.
+ *
+ * @since 0.8
+ */
+ public abstract <T> T match(
+ Function<? super Cumulative, T> p0,
+ Function<? super Interval, T> p1,
+ Function<? super AggregationWindow, T> defaultFunction);
+
+ /**
+ * Cumulative (infinite interval) time {@code AggregationWindow}.
+ *
+ * @since 0.8
+ * @deprecated since 0.13. In the future all {@link View}s will be cumulative.
+ */
+ @Deprecated
+ @Immutable
+ @AutoValue
+ @AutoValue.CopyAnnotations
+ public abstract static class Cumulative extends AggregationWindow {
+
+ private static final Cumulative CUMULATIVE =
+ new AutoValue_View_AggregationWindow_Cumulative();
+
+ Cumulative() {}
+
+ /**
+ * Constructs a cumulative {@code AggregationWindow} that does not have an explicit {@code
+ * Duration}. Instead, cumulative {@code AggregationWindow} always has an interval of infinite
+ * {@code Duration}.
+ *
+ * @return a cumulative {@code AggregationWindow}.
+ * @since 0.8
+ */
+ public static Cumulative create() {
+ return CUMULATIVE;
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super Cumulative, T> p0,
+ Function<? super Interval, T> p1,
+ Function<? super AggregationWindow, T> defaultFunction) {
+ return p0.apply(this);
+ }
+ }
+
+ /**
+ * Interval (finite interval) time {@code AggregationWindow}.
+ *
+ * @since 0.8
+ * @deprecated since 0.13. In the future all {@link View}s will be cumulative.
+ */
+ @Deprecated
+ @Immutable
+ @AutoValue
+ @AutoValue.CopyAnnotations
+ public abstract static class Interval extends AggregationWindow {
+
+ private static final Duration ZERO = Duration.create(0, 0);
+
+ Interval() {}
+
+ /**
+ * Returns the {@code Duration} associated with this {@code Interval}.
+ *
+ * @return a {@code Duration}.
+ * @since 0.8
+ */
+ public abstract Duration getDuration();
+
+ /**
+ * Constructs an interval {@code AggregationWindow} that has a finite explicit {@code
+ * Duration}.
+ *
+ * <p>The {@code Duration} should be able to round to milliseconds. Currently interval window
+ * cannot have smaller {@code Duration} such as microseconds or nanoseconds.
+ *
+ * @return an interval {@code AggregationWindow}.
+ * @since 0.8
+ */
+ public static Interval create(Duration duration) {
+ Utils.checkArgument(duration.compareTo(ZERO) > 0, "Duration must be positive");
+ return new AutoValue_View_AggregationWindow_Interval(duration);
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super Cumulative, T> p0,
+ Function<? super Interval, T> p1,
+ Function<? super AggregationWindow, T> defaultFunction) {
+ return p1.apply(this);
+ }
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/stats/ViewData.java b/api/src/main/java/io/opencensus/stats/ViewData.java
new file mode 100644
index 00000000..df6edaa7
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/ViewData.java
@@ -0,0 +1,461 @@
+/*
+ * 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.stats;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.concurrent.Immutable;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * The aggregated data for a particular {@link View}.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+@AutoValue.CopyAnnotations
+@SuppressWarnings("deprecation")
+public abstract class ViewData {
+
+ // Prevents this class from being subclassed anywhere else.
+ ViewData() {}
+
+ /**
+ * The {@link View} associated with this {@link ViewData}.
+ *
+ * @since 0.8
+ */
+ public abstract View getView();
+
+ /**
+ * The {@link AggregationData} grouped by combination of tag values, associated with this {@link
+ * ViewData}.
+ *
+ * @since 0.8
+ */
+ public abstract Map<List</*@Nullable*/ TagValue>, AggregationData> getAggregationMap();
+
+ /**
+ * Returns the {@link AggregationWindowData} associated with this {@link ViewData}.
+ *
+ * <p>{@link AggregationWindowData} is deprecated since 0.13, please avoid using this method. Use
+ * {@link #getStart()} and {@link #getEnd()} instead.
+ *
+ * @return the {@code AggregationWindowData}.
+ * @since 0.8
+ * @deprecated in favor of {@link #getStart()} and {@link #getEnd()}.
+ */
+ @Deprecated
+ public abstract AggregationWindowData getWindowData();
+
+ /**
+ * Returns the start {@code Timestamp} for a {@link ViewData}.
+ *
+ * @return the start {@code Timestamp}.
+ * @since 0.13
+ */
+ public abstract Timestamp getStart();
+
+ /**
+ * Returns the end {@code Timestamp} for a {@link ViewData}.
+ *
+ * @return the end {@code Timestamp}.
+ * @since 0.13
+ */
+ public abstract Timestamp getEnd();
+
+ /**
+ * Constructs a new {@link ViewData}.
+ *
+ * @param view the {@link View} associated with this {@link ViewData}.
+ * @param map the mapping from {@link TagValue} list to {@link AggregationData}.
+ * @param windowData the {@link AggregationWindowData}.
+ * @return a {@code ViewData}.
+ * @throws IllegalArgumentException if the types of {@code Aggregation} and {@code
+ * AggregationData} don't match, or the types of {@code Window} and {@code WindowData} don't
+ * match.
+ * @since 0.8
+ * @deprecated in favor of {@link #create(View, Map, Timestamp, Timestamp)}.
+ */
+ @Deprecated
+ public static ViewData create(
+ final View view,
+ Map<? extends List</*@Nullable*/ TagValue>, ? extends AggregationData> map,
+ final AggregationWindowData windowData) {
+ checkWindow(view.getWindow(), windowData);
+ final Map<List</*@Nullable*/ TagValue>, AggregationData> deepCopy =
+ new HashMap<List</*@Nullable*/ TagValue>, AggregationData>();
+ for (Entry<? extends List</*@Nullable*/ TagValue>, ? extends AggregationData> entry :
+ map.entrySet()) {
+ checkAggregation(view.getAggregation(), entry.getValue(), view.getMeasure());
+ deepCopy.put(
+ Collections.unmodifiableList(new ArrayList</*@Nullable*/ TagValue>(entry.getKey())),
+ entry.getValue());
+ }
+ return windowData.match(
+ new Function<ViewData.AggregationWindowData.CumulativeData, ViewData>() {
+ @Override
+ public ViewData apply(ViewData.AggregationWindowData.CumulativeData arg) {
+ return createInternal(
+ view, Collections.unmodifiableMap(deepCopy), arg, arg.getStart(), arg.getEnd());
+ }
+ },
+ new Function<ViewData.AggregationWindowData.IntervalData, ViewData>() {
+ @Override
+ public ViewData apply(ViewData.AggregationWindowData.IntervalData arg) {
+ Duration duration = ((View.AggregationWindow.Interval) view.getWindow()).getDuration();
+ return createInternal(
+ view,
+ Collections.unmodifiableMap(deepCopy),
+ arg,
+ arg.getEnd()
+ .addDuration(Duration.create(-duration.getSeconds(), -duration.getNanos())),
+ arg.getEnd());
+ }
+ },
+ Functions.<ViewData>throwAssertionError());
+ }
+
+ /**
+ * Constructs a new {@link ViewData}.
+ *
+ * @param view the {@link View} associated with this {@link ViewData}.
+ * @param map the mapping from {@link TagValue} list to {@link AggregationData}.
+ * @param start the start {@link Timestamp} for this {@link ViewData}.
+ * @param end the end {@link Timestamp} for this {@link ViewData}.
+ * @return a {@code ViewData}.
+ * @throws IllegalArgumentException if the types of {@code Aggregation} and {@code
+ * AggregationData} don't match.
+ * @since 0.13
+ */
+ public static ViewData create(
+ View view,
+ Map<? extends List</*@Nullable*/ TagValue>, ? extends AggregationData> map,
+ Timestamp start,
+ Timestamp end) {
+ Map<List</*@Nullable*/ TagValue>, AggregationData> deepCopy =
+ new HashMap<List</*@Nullable*/ TagValue>, AggregationData>();
+ for (Entry<? extends List</*@Nullable*/ TagValue>, ? extends AggregationData> entry :
+ map.entrySet()) {
+ checkAggregation(view.getAggregation(), entry.getValue(), view.getMeasure());
+ deepCopy.put(
+ Collections.unmodifiableList(new ArrayList</*@Nullable*/ TagValue>(entry.getKey())),
+ entry.getValue());
+ }
+ return createInternal(
+ view,
+ Collections.unmodifiableMap(deepCopy),
+ AggregationWindowData.CumulativeData.create(start, end),
+ start,
+ end);
+ }
+
+ // Suppresses a nullness warning about calls to the AutoValue_ViewData constructor. The generated
+ // constructor does not have the @Nullable annotation on TagValue.
+ private static ViewData createInternal(
+ View view,
+ Map<List</*@Nullable*/ TagValue>, AggregationData> aggregationMap,
+ AggregationWindowData window,
+ Timestamp start,
+ Timestamp end) {
+ @SuppressWarnings("nullness")
+ Map<List<TagValue>, AggregationData> map = aggregationMap;
+ return new AutoValue_ViewData(view, map, window, start, end);
+ }
+
+ private static void checkWindow(
+ View.AggregationWindow window, final AggregationWindowData windowData) {
+ window.match(
+ new Function<View.AggregationWindow.Cumulative, Void>() {
+ @Override
+ public Void apply(View.AggregationWindow.Cumulative arg) {
+ throwIfWindowMismatch(
+ windowData instanceof AggregationWindowData.CumulativeData, arg, windowData);
+ return null;
+ }
+ },
+ new Function<View.AggregationWindow.Interval, Void>() {
+ @Override
+ public Void apply(View.AggregationWindow.Interval arg) {
+ throwIfWindowMismatch(
+ windowData instanceof AggregationWindowData.IntervalData, arg, windowData);
+ return null;
+ }
+ },
+ Functions.</*@Nullable*/ Void>throwAssertionError());
+ }
+
+ private static void throwIfWindowMismatch(
+ boolean isValid, View.AggregationWindow window, AggregationWindowData windowData) {
+ if (!isValid) {
+ throw new IllegalArgumentException(createErrorMessageForWindow(window, windowData));
+ }
+ }
+
+ private static String createErrorMessageForWindow(
+ View.AggregationWindow window, AggregationWindowData windowData) {
+ return "AggregationWindow and AggregationWindowData types mismatch. "
+ + "AggregationWindow: "
+ + window.getClass().getSimpleName()
+ + " AggregationWindowData: "
+ + windowData.getClass().getSimpleName();
+ }
+
+ private static void checkAggregation(
+ final Aggregation aggregation, final AggregationData aggregationData, final Measure measure) {
+ aggregation.match(
+ new Function<Sum, Void>() {
+ @Override
+ public Void apply(Sum arg) {
+ measure.match(
+ new Function<MeasureDouble, Void>() {
+ @Override
+ public Void apply(MeasureDouble arg) {
+ throwIfAggregationMismatch(
+ aggregationData instanceof SumDataDouble, aggregation, aggregationData);
+ return null;
+ }
+ },
+ new Function<MeasureLong, Void>() {
+ @Override
+ public Void apply(MeasureLong arg) {
+ throwIfAggregationMismatch(
+ aggregationData instanceof SumDataLong, aggregation, aggregationData);
+ return null;
+ }
+ },
+ Functions.</*@Nullable*/ Void>throwAssertionError());
+ return null;
+ }
+ },
+ new Function<Count, Void>() {
+ @Override
+ public Void apply(Count arg) {
+ throwIfAggregationMismatch(
+ aggregationData instanceof CountData, aggregation, aggregationData);
+ return null;
+ }
+ },
+ new Function<Distribution, Void>() {
+ @Override
+ public Void apply(Distribution arg) {
+ throwIfAggregationMismatch(
+ aggregationData instanceof DistributionData, aggregation, aggregationData);
+ return null;
+ }
+ },
+ new Function<LastValue, Void>() {
+ @Override
+ public Void apply(LastValue arg) {
+ measure.match(
+ new Function<MeasureDouble, Void>() {
+ @Override
+ public Void apply(MeasureDouble arg) {
+ throwIfAggregationMismatch(
+ aggregationData instanceof LastValueDataDouble,
+ aggregation,
+ aggregationData);
+ return null;
+ }
+ },
+ new Function<MeasureLong, Void>() {
+ @Override
+ public Void apply(MeasureLong arg) {
+ throwIfAggregationMismatch(
+ aggregationData instanceof LastValueDataLong, aggregation, aggregationData);
+ return null;
+ }
+ },
+ Functions.</*@Nullable*/ Void>throwAssertionError());
+ return null;
+ }
+ },
+ new Function<Aggregation, Void>() {
+ @Override
+ public Void apply(Aggregation arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof Aggregation.Mean) {
+ throwIfAggregationMismatch(
+ aggregationData instanceof AggregationData.MeanData,
+ aggregation,
+ aggregationData);
+ return null;
+ }
+ throw new AssertionError();
+ }
+ });
+ }
+
+ private static void throwIfAggregationMismatch(
+ boolean isValid, Aggregation aggregation, AggregationData aggregationData) {
+ if (!isValid) {
+ throw new IllegalArgumentException(
+ createErrorMessageForAggregation(aggregation, aggregationData));
+ }
+ }
+
+ private static String createErrorMessageForAggregation(
+ Aggregation aggregation, AggregationData aggregationData) {
+ return "Aggregation and AggregationData types mismatch. "
+ + "Aggregation: "
+ + aggregation.getClass().getSimpleName()
+ + " AggregationData: "
+ + aggregationData.getClass().getSimpleName();
+ }
+
+ /**
+ * The {@code AggregationWindowData} for a {@link ViewData}.
+ *
+ * @since 0.8
+ * @deprecated since 0.13, please use start and end {@link Timestamp} instead.
+ */
+ @Deprecated
+ @Immutable
+ public abstract static class AggregationWindowData {
+
+ private AggregationWindowData() {}
+
+ /**
+ * Applies the given match function to the underlying data type.
+ *
+ * @since 0.8
+ */
+ public abstract <T> T match(
+ Function<? super CumulativeData, T> p0,
+ Function<? super IntervalData, T> p1,
+ Function<? super AggregationWindowData, T> defaultFunction);
+
+ /**
+ * Cumulative {@code AggregationWindowData}.
+ *
+ * @since 0.8
+ * @deprecated since 0.13, please use start and end {@link Timestamp} instead.
+ */
+ @Deprecated
+ @Immutable
+ @AutoValue
+ @AutoValue.CopyAnnotations
+ public abstract static class CumulativeData extends AggregationWindowData {
+
+ CumulativeData() {}
+
+ /**
+ * Returns the start {@code Timestamp} for a {@link CumulativeData}.
+ *
+ * @return the start {@code Timestamp}.
+ * @since 0.8
+ */
+ public abstract Timestamp getStart();
+
+ /**
+ * Returns the end {@code Timestamp} for a {@link CumulativeData}.
+ *
+ * @return the end {@code Timestamp}.
+ * @since 0.8
+ */
+ public abstract Timestamp getEnd();
+
+ @Override
+ public final <T> T match(
+ Function<? super CumulativeData, T> p0,
+ Function<? super IntervalData, T> p1,
+ Function<? super AggregationWindowData, T> defaultFunction) {
+ return p0.apply(this);
+ }
+
+ /**
+ * Constructs a new {@link CumulativeData}.
+ *
+ * @since 0.8
+ */
+ public static CumulativeData create(Timestamp start, Timestamp end) {
+ if (start.compareTo(end) > 0) {
+ throw new IllegalArgumentException("Start time is later than end time.");
+ }
+ return new AutoValue_ViewData_AggregationWindowData_CumulativeData(start, end);
+ }
+ }
+
+ /**
+ * Interval {@code AggregationWindowData}.
+ *
+ * @since 0.8
+ * @deprecated since 0.13, please use start and end {@link Timestamp} instead.
+ */
+ @Deprecated
+ @Immutable
+ @AutoValue
+ @AutoValue.CopyAnnotations
+ public abstract static class IntervalData extends AggregationWindowData {
+
+ IntervalData() {}
+
+ /**
+ * Returns the end {@code Timestamp} for an {@link IntervalData}.
+ *
+ * @return the end {@code Timestamp}.
+ * @since 0.8
+ */
+ public abstract Timestamp getEnd();
+
+ @Override
+ public final <T> T match(
+ Function<? super CumulativeData, T> p0,
+ Function<? super IntervalData, T> p1,
+ Function<? super AggregationWindowData, T> defaultFunction) {
+ return p1.apply(this);
+ }
+
+ /**
+ * Constructs a new {@link IntervalData}.
+ *
+ * @since 0.8
+ */
+ public static IntervalData create(Timestamp end) {
+ return new AutoValue_ViewData_AggregationWindowData_IntervalData(end);
+ }
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/stats/ViewManager.java b/api/src/main/java/io/opencensus/stats/ViewManager.java
new file mode 100644
index 00000000..a00165cc
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/ViewManager.java
@@ -0,0 +1,61 @@
+/*
+ * 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.stats;
+
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * Provides facilities to register {@link View}s for collecting stats and retrieving stats data as a
+ * {@link ViewData}.
+ *
+ * @since 0.8
+ */
+public abstract class ViewManager {
+ /**
+ * Pull model for stats. Registers a {@link View} that will collect data to be accessed via {@link
+ * #getView(View.Name)}.
+ *
+ * @param view the {@code View} to be registered.
+ * @since 0.8
+ */
+ public abstract void registerView(View view);
+
+ /**
+ * Returns the current stats data, {@link ViewData}, associated with the given view name.
+ *
+ * <p>Returns {@code null} if the {@code View} is not registered.
+ *
+ * @param view the name of {@code View} for the current stats.
+ * @return {@code ViewData} for the {@code View}, or {@code null} if the {@code View} is not
+ * registered.
+ * @since 0.8
+ */
+ @Nullable
+ public abstract ViewData getView(View.Name view);
+
+ /**
+ * Returns all registered views that should be exported.
+ *
+ * <p>This method should be used by any stats exporter that automatically exports data for views
+ * registered with the {@link ViewManager}.
+ *
+ * @return all registered views that should be exported.
+ * @since 0.9
+ */
+ public abstract Set<View> getAllExportedViews();
+}
diff --git a/api/src/main/java/io/opencensus/stats/package-info.java b/api/src/main/java/io/opencensus/stats/package-info.java
new file mode 100644
index 00000000..981daa0e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/stats/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/** API for stats recording. */
+// TODO: Add more details.
+// TODO: Add code examples.
+package io.opencensus.stats;
diff --git a/api/src/main/java/io/opencensus/tags/InternalUtils.java b/api/src/main/java/io/opencensus/tags/InternalUtils.java
new file mode 100644
index 00000000..944122e1
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/InternalUtils.java
@@ -0,0 +1,38 @@
+/*
+ * 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.tags;
+
+import java.util.Iterator;
+
+/**
+ * Internal tagging utilities.
+ *
+ * @since 0.8
+ */
+@io.opencensus.common.Internal
+public final class InternalUtils {
+ private InternalUtils() {}
+
+ /**
+ * Internal tag accessor.
+ *
+ * @since 0.8
+ */
+ public static Iterator<Tag> getTags(TagContext tags) {
+ return tags.getIterator();
+ }
+}
diff --git a/api/src/main/java/io/opencensus/tags/NoopTags.java b/api/src/main/java/io/opencensus/tags/NoopTags.java
new file mode 100644
index 00000000..fb52b164
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/NoopTags.java
@@ -0,0 +1,214 @@
+/*
+ * 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.tags;
+
+import io.opencensus.common.Scope;
+import io.opencensus.internal.NoopScope;
+import io.opencensus.internal.Utils;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+import java.util.Collections;
+import java.util.Iterator;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** No-op implementations of tagging classes. */
+final class NoopTags {
+
+ private NoopTags() {}
+
+ /**
+ * Returns a {@code TagsComponent} that has a no-op implementation for {@link Tagger}.
+ *
+ * @return a {@code TagsComponent} that has a no-op implementation for {@code Tagger}.
+ */
+ static TagsComponent newNoopTagsComponent() {
+ return new NoopTagsComponent();
+ }
+
+ /**
+ * Returns a {@code Tagger} that only produces {@link TagContext}s with no tags.
+ *
+ * @return a {@code Tagger} that only produces {@code TagContext}s with no tags.
+ */
+ static Tagger getNoopTagger() {
+ return NoopTagger.INSTANCE;
+ }
+
+ /**
+ * Returns a {@code TagContextBuilder} that ignores all calls to {@link TagContextBuilder#put}.
+ *
+ * @return a {@code TagContextBuilder} that ignores all calls to {@link TagContextBuilder#put}.
+ */
+ static TagContextBuilder getNoopTagContextBuilder() {
+ return NoopTagContextBuilder.INSTANCE;
+ }
+
+ /**
+ * Returns a {@code TagContext} that does not contain any tags.
+ *
+ * @return a {@code TagContext} that does not contain any tags.
+ */
+ static TagContext getNoopTagContext() {
+ return NoopTagContext.INSTANCE;
+ }
+
+ /** Returns a {@code TagPropagationComponent} that contains no-op serializers. */
+ static TagPropagationComponent getNoopTagPropagationComponent() {
+ return NoopTagPropagationComponent.INSTANCE;
+ }
+
+ /**
+ * Returns a {@code TagContextBinarySerializer} that serializes all {@code TagContext}s to zero
+ * bytes and deserializes all inputs to empty {@code TagContext}s.
+ */
+ static TagContextBinarySerializer getNoopTagContextBinarySerializer() {
+ return NoopTagContextBinarySerializer.INSTANCE;
+ }
+
+ @ThreadSafe
+ private static final class NoopTagsComponent extends TagsComponent {
+ private volatile boolean isRead;
+
+ @Override
+ public Tagger getTagger() {
+ return getNoopTagger();
+ }
+
+ @Override
+ public TagPropagationComponent getTagPropagationComponent() {
+ return getNoopTagPropagationComponent();
+ }
+
+ @Override
+ public TaggingState getState() {
+ isRead = true;
+ return TaggingState.DISABLED;
+ }
+
+ @Override
+ @Deprecated
+ public void setState(TaggingState state) {
+ Utils.checkNotNull(state, "state");
+ Utils.checkState(!isRead, "State was already read, cannot set state.");
+ }
+ }
+
+ @Immutable
+ private static final class NoopTagger extends Tagger {
+ static final Tagger INSTANCE = new NoopTagger();
+
+ @Override
+ public TagContext empty() {
+ return getNoopTagContext();
+ }
+
+ @Override
+ public TagContext getCurrentTagContext() {
+ return getNoopTagContext();
+ }
+
+ @Override
+ public TagContextBuilder emptyBuilder() {
+ return getNoopTagContextBuilder();
+ }
+
+ @Override
+ public TagContextBuilder toBuilder(TagContext tags) {
+ Utils.checkNotNull(tags, "tags");
+ return getNoopTagContextBuilder();
+ }
+
+ @Override
+ public TagContextBuilder currentBuilder() {
+ return getNoopTagContextBuilder();
+ }
+
+ @Override
+ public Scope withTagContext(TagContext tags) {
+ Utils.checkNotNull(tags, "tags");
+ return NoopScope.getInstance();
+ }
+ }
+
+ @Immutable
+ private static final class NoopTagContextBuilder extends TagContextBuilder {
+ static final TagContextBuilder INSTANCE = new NoopTagContextBuilder();
+
+ @Override
+ public TagContextBuilder put(TagKey key, TagValue value) {
+ Utils.checkNotNull(key, "key");
+ Utils.checkNotNull(value, "value");
+ return this;
+ }
+
+ @Override
+ public TagContextBuilder remove(TagKey key) {
+ Utils.checkNotNull(key, "key");
+ return this;
+ }
+
+ @Override
+ public TagContext build() {
+ return getNoopTagContext();
+ }
+
+ @Override
+ public Scope buildScoped() {
+ return NoopScope.getInstance();
+ }
+ }
+
+ @Immutable
+ private static final class NoopTagContext extends TagContext {
+ static final TagContext INSTANCE = new NoopTagContext();
+
+ // TODO(sebright): Is there any way to let the user know that their tags were ignored?
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return Collections.<Tag>emptySet().iterator();
+ }
+ }
+
+ @Immutable
+ private static final class NoopTagPropagationComponent extends TagPropagationComponent {
+ static final TagPropagationComponent INSTANCE = new NoopTagPropagationComponent();
+
+ @Override
+ public TagContextBinarySerializer getBinarySerializer() {
+ return getNoopTagContextBinarySerializer();
+ }
+ }
+
+ @Immutable
+ private static final class NoopTagContextBinarySerializer extends TagContextBinarySerializer {
+ static final TagContextBinarySerializer INSTANCE = new NoopTagContextBinarySerializer();
+ static final byte[] EMPTY_BYTE_ARRAY = {};
+
+ @Override
+ public byte[] toByteArray(TagContext tags) {
+ Utils.checkNotNull(tags, "tags");
+ return EMPTY_BYTE_ARRAY;
+ }
+
+ @Override
+ public TagContext fromByteArray(byte[] bytes) {
+ Utils.checkNotNull(bytes, "bytes");
+ return getNoopTagContext();
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/tags/Tag.java b/api/src/main/java/io/opencensus/tags/Tag.java
new file mode 100644
index 00000000..9e0a7a82
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/Tag.java
@@ -0,0 +1,60 @@
+/*
+ * 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.tags;
+
+import com.google.auto.value.AutoValue;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link TagKey} paired with a {@link TagValue}.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+public abstract class Tag {
+
+ Tag() {}
+
+ /**
+ * Creates a {@code Tag} from the given key and value.
+ *
+ * @param key the tag key.
+ * @param value the tag value.
+ * @return a {@code Tag} with the given key and value.
+ * @since 0.8
+ */
+ public static Tag create(TagKey key, TagValue value) {
+ return new AutoValue_Tag(key, value);
+ }
+
+ /**
+ * Returns the tag's key.
+ *
+ * @return the tag's key.
+ * @since 0.8
+ */
+ public abstract TagKey getKey();
+
+ /**
+ * Returns the tag's value.
+ *
+ * @return the tag's value.
+ * @since 0.8
+ */
+ public abstract TagValue getValue();
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagContext.java b/api/src/main/java/io/opencensus/tags/TagContext.java
new file mode 100644
index 00000000..e36acdff
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagContext.java
@@ -0,0 +1,109 @@
+/*
+ * 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.tags;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A map from {@link TagKey} to {@link TagValue} that can be used to label anything that is
+ * associated with a specific operation.
+ *
+ * <p>For example, {@code TagContext}s can be used to label stats, log messages, or debugging
+ * information.
+ *
+ * @since 0.8
+ */
+@Immutable
+public abstract class TagContext {
+
+ /**
+ * Returns an iterator over the tags in this {@code TagContext}.
+ *
+ * @return an iterator over the tags in this {@code TagContext}.
+ * @since 0.8
+ */
+ // This method is protected to prevent client code from accessing the tags of any TagContext. We
+ // don't currently support efficient access to tags. However, every TagContext subclass needs to
+ // provide access to its tags to the stats and tagging implementations by implementing this
+ // method. If we decide to support access to tags in the future, we can add a public iterator()
+ // method and implement it for all subclasses by calling getIterator().
+ //
+ // The stats and tagging implementations can access any TagContext's tags through
+ // io.opencensus.tags.InternalUtils.getTags, which calls this method.
+ protected abstract Iterator<Tag> getIterator();
+
+ @Override
+ public String toString() {
+ return "TagContext";
+ }
+
+ /**
+ * Returns true iff the other object is an instance of {@code TagContext} and contains the same
+ * key-value pairs. Implementations are free to override this method to provide better
+ * performance.
+ */
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (!(other instanceof TagContext)) {
+ return false;
+ }
+ TagContext otherTags = (TagContext) other;
+ Iterator<Tag> iter1 = getIterator();
+ Iterator<Tag> iter2 = otherTags.getIterator();
+ HashMap<Tag, Integer> tags = new HashMap<Tag, Integer>();
+ while (iter1 != null && iter1.hasNext()) {
+ Tag tag = iter1.next();
+ if (tags.containsKey(tag)) {
+ tags.put(tag, tags.get(tag) + 1);
+ } else {
+ tags.put(tag, 1);
+ }
+ }
+ while (iter2 != null && iter2.hasNext()) {
+ Tag tag = iter2.next();
+ if (!tags.containsKey(tag)) {
+ return false;
+ }
+ int count = tags.get(tag);
+ if (count > 1) {
+ tags.put(tag, count - 1);
+ } else {
+ tags.remove(tag);
+ }
+ }
+ return tags.isEmpty();
+ }
+
+ @Override
+ public final int hashCode() {
+ int hashCode = 0;
+ Iterator<Tag> i = getIterator();
+ if (i == null) {
+ return hashCode;
+ }
+ while (i.hasNext()) {
+ Tag tag = i.next();
+ if (tag != null) {
+ hashCode += tag.hashCode();
+ }
+ }
+ return hashCode;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagContextBuilder.java b/api/src/main/java/io/opencensus/tags/TagContextBuilder.java
new file mode 100644
index 00000000..f4268968
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagContextBuilder.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.tags;
+
+import io.opencensus.common.Scope;
+
+/**
+ * Builder for the {@link TagContext} class.
+ *
+ * @since 0.8
+ */
+public abstract class TagContextBuilder {
+
+ /**
+ * Adds the key/value pair regardless of whether the key is present.
+ *
+ * @param key the {@code TagKey} which will be set.
+ * @param value the {@code TagValue} to set for the given key.
+ * @return this
+ * @since 0.8
+ */
+ public abstract TagContextBuilder put(TagKey key, TagValue value);
+
+ /**
+ * Removes the key if it exists.
+ *
+ * @param key the {@code TagKey} which will be removed.
+ * @return this
+ * @since 0.8
+ */
+ public abstract TagContextBuilder remove(TagKey key);
+
+ /**
+ * Creates a {@code TagContext} from this builder.
+ *
+ * @return a {@code TagContext} with the same tags as this builder.
+ * @since 0.8
+ */
+ public abstract TagContext build();
+
+ /**
+ * Enters the scope of code where the {@link TagContext} created from this builder is in the
+ * current context and returns an object that represents that scope. The scope is exited when the
+ * returned object is closed.
+ *
+ * @return an object that defines a scope where the {@code TagContext} created from this builder
+ * is set to the current context.
+ * @since 0.8
+ */
+ public abstract Scope buildScoped();
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagKey.java b/api/src/main/java/io/opencensus/tags/TagKey.java
new file mode 100644
index 00000000..ca4582bd
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagKey.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.tags;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.StringUtils;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A key to a value stored in a {@link TagContext}.
+ *
+ * <p>Each {@code TagKey} has a {@code String} name. Names have a maximum length of {@link
+ * #MAX_LENGTH} and contain only printable ASCII characters.
+ *
+ * <p>{@code TagKey}s are designed to be used as constants. Declaring each key as a constant
+ * prevents key names from being validated multiple times.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+public abstract class TagKey {
+ /**
+ * The maximum length for a tag key name. The value is {@value #MAX_LENGTH}.
+ *
+ * @since 0.8
+ */
+ public static final int MAX_LENGTH = 255;
+
+ TagKey() {}
+
+ /**
+ * Constructs a {@code TagKey} with the given name.
+ *
+ * <p>The name must meet the following requirements:
+ *
+ * <ol>
+ * <li>It cannot be longer than {@link #MAX_LENGTH}.
+ * <li>It can only contain printable ASCII characters.
+ * </ol>
+ *
+ * @param name the name of the key.
+ * @return a {@code TagKey} with the given name.
+ * @throws IllegalArgumentException if the name is not valid.
+ * @since 0.8
+ */
+ public static TagKey create(String name) {
+ Utils.checkArgument(isValid(name), "Invalid TagKey name: %s", name);
+ return new AutoValue_TagKey(name);
+ }
+
+ /**
+ * Returns the name of the key.
+ *
+ * @return the name of the key.
+ * @since 0.8
+ */
+ public abstract String getName();
+
+ /**
+ * Determines whether the given {@code String} is a valid tag key.
+ *
+ * @param name the tag key name to be validated.
+ * @return whether the name is valid.
+ */
+ private static boolean isValid(String name) {
+ return !name.isEmpty() && name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name);
+ }
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagValue.java b/api/src/main/java/io/opencensus/tags/TagValue.java
new file mode 100644
index 00000000..9111ca28
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagValue.java
@@ -0,0 +1,79 @@
+/*
+ * 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.tags;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.StringUtils;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A validated tag value.
+ *
+ * <p>Validation ensures that the {@code String} has a maximum length of {@link #MAX_LENGTH} and
+ * contains only printable ASCII characters.
+ *
+ * @since 0.8
+ */
+@Immutable
+@AutoValue
+public abstract class TagValue {
+ /**
+ * The maximum length for a tag value. The value is {@value #MAX_LENGTH}.
+ *
+ * @since 0.8
+ */
+ public static final int MAX_LENGTH = 255;
+
+ TagValue() {}
+
+ /**
+ * Constructs a {@code TagValue} from the given string. The string must meet the following
+ * requirements:
+ *
+ * <ol>
+ * <li>It cannot be longer than {@link #MAX_LENGTH}.
+ * <li>It can only contain printable ASCII characters.
+ * </ol>
+ *
+ * @param value the tag value.
+ * @throws IllegalArgumentException if the {@code String} is not valid.
+ * @since 0.8
+ */
+ public static TagValue create(String value) {
+ Utils.checkArgument(isValid(value), "Invalid TagValue: %s", value);
+ return new AutoValue_TagValue(value);
+ }
+
+ /**
+ * Returns the tag value as a {@code String}.
+ *
+ * @return the tag value as a {@code String}.
+ * @since 0.8
+ */
+ public abstract String asString();
+
+ /**
+ * Determines whether the given {@code String} is a valid tag value.
+ *
+ * @param value the tag value to be validated.
+ * @return whether the value is valid.
+ */
+ private static boolean isValid(String value) {
+ return value.length() <= MAX_LENGTH && StringUtils.isPrintableString(value);
+ }
+}
diff --git a/api/src/main/java/io/opencensus/tags/Tagger.java b/api/src/main/java/io/opencensus/tags/Tagger.java
new file mode 100644
index 00000000..f1e203ad
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/Tagger.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.tags;
+
+import io.opencensus.common.Scope;
+
+/**
+ * Object for creating new {@link TagContext}s and {@code TagContext}s based on the current context.
+ *
+ * <p>This class returns {@link TagContextBuilder builders} that can be used to create the
+ * implementation-dependent {@link TagContext}s.
+ *
+ * <p>Implementations may have different constraints and are free to convert tag contexts to their
+ * own subtypes. This means callers cannot assume the {@link #getCurrentTagContext() current
+ * context} is the same instance as the one {@link #withTagContext(TagContext) placed into scope}.
+ *
+ * @since 0.8
+ */
+public abstract class Tagger {
+
+ /**
+ * Returns an empty {@code TagContext}.
+ *
+ * @return an empty {@code TagContext}.
+ * @since 0.8
+ */
+ public abstract TagContext empty();
+
+ /**
+ * Returns the current {@code TagContext}.
+ *
+ * @return the current {@code TagContext}.
+ * @since 0.8
+ */
+ public abstract TagContext getCurrentTagContext();
+
+ /**
+ * Returns a new empty {@code Builder}.
+ *
+ * @return a new empty {@code Builder}.
+ * @since 0.8
+ */
+ public abstract TagContextBuilder emptyBuilder();
+
+ /**
+ * Returns a builder based on this {@code TagContext}.
+ *
+ * @return a builder based on this {@code TagContext}.
+ * @since 0.8
+ */
+ public abstract TagContextBuilder toBuilder(TagContext tags);
+
+ /**
+ * Returns a new builder created from the current {@code TagContext}.
+ *
+ * @return a new builder created from the current {@code TagContext}.
+ * @since 0.8
+ */
+ public abstract TagContextBuilder currentBuilder();
+
+ /**
+ * Enters the scope of code where the given {@code TagContext} is in the current context
+ * (replacing the previous {@code TagContext}) 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.
+ * @since 0.8
+ */
+ public abstract Scope withTagContext(TagContext tags);
+}
diff --git a/api/src/main/java/io/opencensus/tags/TaggingState.java b/api/src/main/java/io/opencensus/tags/TaggingState.java
new file mode 100644
index 00000000..88970361
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TaggingState.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.tags;
+
+/**
+ * State of the {@link TagsComponent}.
+ *
+ * @since 0.8
+ */
+public enum TaggingState {
+ // TODO(sebright): Should we add a state that propagates the tags, but doesn't allow
+ // modifications?
+
+ /**
+ * State that fully enables tagging.
+ *
+ * <p>The {@link TagsComponent} can add tags to {@link TagContext}s, propagate {@code TagContext}s
+ * in the current context, and serialize {@code TagContext}s.
+ *
+ * @since 0.8
+ */
+ ENABLED,
+
+ /**
+ * State that disables tagging.
+ *
+ * <p>The {@link TagsComponent} may not add tags to {@link TagContext}s, propagate {@code
+ * TagContext}s in the current context, or serialize {@code TagContext}s.
+ *
+ * @since 0.8
+ */
+ // TODO(sebright): Document how this interacts with stats collection.
+ DISABLED
+}
diff --git a/api/src/main/java/io/opencensus/tags/Tags.java b/api/src/main/java/io/opencensus/tags/Tags.java
new file mode 100644
index 00000000..07123647
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/Tags.java
@@ -0,0 +1,126 @@
+/*
+ * 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.tags;
+
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Provider;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Class for accessing the default {@link TagsComponent}.
+ *
+ * @since 0.8
+ */
+public final class Tags {
+ private static final Logger logger = Logger.getLogger(Tags.class.getName());
+
+ private static final TagsComponent tagsComponent =
+ loadTagsComponent(TagsComponent.class.getClassLoader());
+
+ private Tags() {}
+
+ /**
+ * Returns the default {@code Tagger}.
+ *
+ * @return the default {@code Tagger}.
+ * @since 0.8
+ */
+ public static Tagger getTagger() {
+ return tagsComponent.getTagger();
+ }
+
+ /**
+ * Returns the default {@code TagPropagationComponent}.
+ *
+ * @return the default {@code TagPropagationComponent}.
+ * @since 0.8
+ */
+ public static TagPropagationComponent getTagPropagationComponent() {
+ return tagsComponent.getTagPropagationComponent();
+ }
+
+ /**
+ * Returns the current {@code TaggingState}.
+ *
+ * <p>When no implementation is available, {@code getState} always returns {@link
+ * TaggingState#DISABLED}.
+ *
+ * <p>Once {@link #getState()} is called, subsequent calls to {@link #setState(TaggingState)} will
+ * throw an {@code IllegalStateException}.
+ *
+ * @return the current {@code TaggingState}.
+ * @since 0.8
+ */
+ public static TaggingState getState() {
+ return tagsComponent.getState();
+ }
+
+ /**
+ * Sets the current {@code TaggingState}.
+ *
+ * <p>When no implementation is available, {@code setState} does not change the state.
+ *
+ * @param state the new {@code TaggingState}.
+ * @throws IllegalStateException if {@link #getState()} was previously called.
+ * @deprecated This method is deprecated because other libraries could cache the result of {@link
+ * #getState()}, use a stale value, and behave incorrectly. It is only safe to call early in
+ * initialization. This method throws {@link IllegalStateException} after {@link #getState()}
+ * has been called, in order to limit changes to the result of {@code getState()}.
+ * @since 0.8
+ */
+ @Deprecated
+ public static void setState(TaggingState state) {
+ tagsComponent.setState(state);
+ }
+
+ // Any provider that may be used for TagsComponent can be added here.
+ @DefaultVisibilityForTesting
+ static TagsComponent loadTagsComponent(@Nullable ClassLoader classLoader) {
+ try {
+ // Call Class.forName with literal string name of the class to help shading tools.
+ return Provider.createInstance(
+ Class.forName(
+ "io.opencensus.impl.tags.TagsComponentImpl", /*initialize=*/ true, classLoader),
+ TagsComponent.class);
+ } catch (ClassNotFoundException e) {
+ logger.log(
+ Level.FINE,
+ "Couldn't load full implementation for TagsComponent, now trying to load lite "
+ + "implementation.",
+ e);
+ }
+ try {
+ // Call Class.forName with literal string name of the class to help shading tools.
+ return Provider.createInstance(
+ Class.forName(
+ "io.opencensus.impllite.tags.TagsComponentImplLite",
+ /*initialize=*/ true,
+ classLoader),
+ TagsComponent.class);
+ } catch (ClassNotFoundException e) {
+ logger.log(
+ Level.FINE,
+ "Couldn't load lite implementation for TagsComponent, now using "
+ + "default implementation for TagsComponent.",
+ e);
+ }
+ return NoopTags.newNoopTagsComponent();
+ }
+}
diff --git a/api/src/main/java/io/opencensus/tags/TagsComponent.java b/api/src/main/java/io/opencensus/tags/TagsComponent.java
new file mode 100644
index 00000000..d34f1951
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/TagsComponent.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.tags;
+
+import io.opencensus.tags.propagation.TagPropagationComponent;
+
+/**
+ * Class that holds the implementation for {@link Tagger} and {@link TagPropagationComponent}.
+ *
+ * <p>All objects returned by methods on {@code TagsComponent} are cacheable.
+ *
+ * @since 0.8
+ */
+public abstract class TagsComponent {
+
+ /**
+ * Returns the {@link Tagger} for this implementation.
+ *
+ * @since 0.8
+ */
+ public abstract Tagger getTagger();
+
+ /**
+ * Returns the {@link TagPropagationComponent} for this implementation.
+ *
+ * @since 0.8
+ */
+ public abstract TagPropagationComponent getTagPropagationComponent();
+
+ /**
+ * Returns the current {@code TaggingState}.
+ *
+ * <p>When no implementation is available, {@code getState} always returns {@link
+ * TaggingState#DISABLED}.
+ *
+ * <p>Once {@link #getState()} is called, subsequent calls to {@link #setState(TaggingState)} will
+ * throw an {@code IllegalStateException}.
+ *
+ * @return the current {@code TaggingState}.
+ * @since 0.8
+ */
+ public abstract TaggingState getState();
+
+ /**
+ * Sets the current {@code TaggingState}.
+ *
+ * <p>When no implementation is available, {@code setState} does not change the state.
+ *
+ * @param state the new {@code TaggingState}.
+ * @throws IllegalStateException if {@link #getState()} was previously called.
+ * @deprecated This method is deprecated because other libraries could cache the result of {@link
+ * #getState()}, use a stale value, and behave incorrectly. It is only safe to call early in
+ * initialization. This method throws {@link IllegalStateException} after {@code getState()}
+ * has been called, in order to limit changes to the result of {@code getState()}.
+ * @since 0.8
+ */
+ @Deprecated
+ public abstract void setState(TaggingState state);
+}
diff --git a/api/src/main/java/io/opencensus/tags/package-info.java b/api/src/main/java/io/opencensus/tags/package-info.java
new file mode 100644
index 00000000..eb19ee77
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/package-info.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.
+ */
+
+/**
+ * API for associating tags with scoped operations.
+ *
+ * <p>This package manages a set of tags in the {@code io.grpc.Context}. The tags can be used to
+ * label anything that is associated with a specific operation. For example, the {@code
+ * io.opencensus.stats} package labels all stats with the current tags.
+ *
+ * <p>{@link io.opencensus.tags.Tag Tags} are key-value pairs. The {@link io.opencensus.tags.TagKey
+ * keys} and {@link io.opencensus.tags.TagValue values} are wrapped {@code String}s. They are stored
+ * as a map in a {@link io.opencensus.tags.TagContext}.
+ *
+ * <p>Note that tags are independent of the tracing data that is propagated in the {@code
+ * io.grpc.Context}, such as trace ID.
+ */
+// TODO(sebright): Add code examples.
+package io.opencensus.tags;
diff --git a/api/src/main/java/io/opencensus/tags/propagation/TagContextBinarySerializer.java b/api/src/main/java/io/opencensus/tags/propagation/TagContextBinarySerializer.java
new file mode 100644
index 00000000..39eb8cee
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/propagation/TagContextBinarySerializer.java
@@ -0,0 +1,57 @@
+/*
+ * 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.tags.propagation;
+
+import io.opencensus.tags.TagContext;
+
+/**
+ * Object for serializing and deserializing {@link TagContext}s with the binary format.
+ *
+ * <p>See <a
+ * href="https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/BinaryEncoding.md#tag-context">opencensus-specs</a>
+ * for the specification of the cross-language binary serialization format.
+ *
+ * @since 0.8
+ */
+public abstract class TagContextBinarySerializer {
+
+ /**
+ * Serializes the {@code TagContext} into the on-the-wire representation.
+ *
+ * <p>This method should be the inverse of {@link #fromByteArray}.
+ *
+ * @param tags the {@code TagContext} to serialize.
+ * @return the on-the-wire representation of a {@code TagContext}.
+ * @throws TagContextSerializationException if the result would be larger than the maximum allowed
+ * serialized size.
+ * @since 0.8
+ */
+ public abstract byte[] toByteArray(TagContext tags) throws TagContextSerializationException;
+
+ /**
+ * Creates a {@code TagContext} from the given on-the-wire encoded representation.
+ *
+ * <p>This method should be the inverse of {@link #toByteArray}.
+ *
+ * @param bytes on-the-wire representation of a {@code TagContext}.
+ * @return a {@code TagContext} deserialized from {@code bytes}.
+ * @throws TagContextDeserializationException if there is a parse error, the input contains
+ * invalid tags, or the input is larger than the maximum allowed serialized size.
+ * @since 0.8
+ */
+ public abstract TagContext fromByteArray(byte[] bytes) throws TagContextDeserializationException;
+}
diff --git a/api/src/main/java/io/opencensus/tags/propagation/TagContextDeserializationException.java b/api/src/main/java/io/opencensus/tags/propagation/TagContextDeserializationException.java
new file mode 100644
index 00000000..11dcb59f
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/propagation/TagContextDeserializationException.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.tags.propagation;
+
+import io.opencensus.tags.TagContext;
+
+/**
+ * Exception thrown when a {@link TagContext} cannot be parsed.
+ *
+ * @since 0.8
+ */
+public final class TagContextDeserializationException extends Exception {
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * Constructs a new {@code TagContextParseException} with the given message.
+ *
+ * @param message a message describing the error.
+ * @since 0.8
+ */
+ public TagContextDeserializationException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code TagContextParseException} with the given message and cause.
+ *
+ * @param message a message describing the error.
+ * @param cause the cause of the error.
+ * @since 0.8
+ */
+ public TagContextDeserializationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/api/src/main/java/io/opencensus/tags/propagation/TagContextSerializationException.java b/api/src/main/java/io/opencensus/tags/propagation/TagContextSerializationException.java
new file mode 100644
index 00000000..bb3c9b74
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/propagation/TagContextSerializationException.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.tags.propagation;
+
+import io.opencensus.tags.TagContext;
+
+/**
+ * Exception thrown when a {@link TagContext} cannot be serialized.
+ *
+ * @since 0.8
+ */
+public final class TagContextSerializationException extends Exception {
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * Constructs a new {@code TagContextSerializationException} with the given message.
+ *
+ * @param message a message describing the error.
+ * @since 0.8
+ */
+ public TagContextSerializationException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code TagContextSerializationException} with the given message and cause.
+ *
+ * @param message a message describing the error.
+ * @param cause the cause of the error.
+ * @since 0.8
+ */
+ public TagContextSerializationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/api/src/main/java/io/opencensus/tags/propagation/TagPropagationComponent.java b/api/src/main/java/io/opencensus/tags/propagation/TagPropagationComponent.java
new file mode 100644
index 00000000..6ececa79
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/propagation/TagPropagationComponent.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.tags.propagation;
+
+import io.opencensus.tags.TagContext;
+
+/**
+ * Object containing all supported {@link TagContext} propagation formats.
+ *
+ * @since 0.8
+ */
+// TODO(sebright): Add an HTTP serializer.
+public abstract class TagPropagationComponent {
+
+ /**
+ * Returns the {@link TagContextBinarySerializer} for this implementation.
+ *
+ * @return the {@code TagContextBinarySerializer} for this implementation.
+ * @since 0.8
+ */
+ public abstract TagContextBinarySerializer getBinarySerializer();
+}
diff --git a/api/src/main/java/io/opencensus/tags/unsafe/ContextUtils.java b/api/src/main/java/io/opencensus/tags/unsafe/ContextUtils.java
new file mode 100644
index 00000000..8936bbbb
--- /dev/null
+++ b/api/src/main/java/io/opencensus/tags/unsafe/ContextUtils.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.tags.unsafe;
+
+import io.grpc.Context;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import java.util.Collections;
+import java.util.Iterator;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Utility methods for accessing the {@link TagContext} contained in the {@link io.grpc.Context}.
+ *
+ * <p>Most code should interact with the current context via the public APIs in {@link
+ * io.opencensus.tags.TagContext} and avoid accessing {@link #TAG_CONTEXT_KEY} directly.
+ *
+ * @since 0.8
+ */
+public final class ContextUtils {
+ private static final TagContext EMPTY_TAG_CONTEXT = new EmptyTagContext();
+
+ private ContextUtils() {}
+
+ /**
+ * The {@link io.grpc.Context.Key} used to interact with the {@code TagContext} contained in the
+ * {@link io.grpc.Context}.
+ *
+ * @since 0.8
+ */
+ public static final Context.Key<TagContext> TAG_CONTEXT_KEY =
+ Context.keyWithDefault("opencensus-tag-context-key", EMPTY_TAG_CONTEXT);
+
+ @Immutable
+ private static final class EmptyTagContext extends TagContext {
+
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return Collections.<Tag>emptySet().iterator();
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/Annotation.java b/api/src/main/java/io/opencensus/trace/Annotation.java
new file mode 100644
index 00000000..97f2fdd2
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Annotation.java
@@ -0,0 +1,83 @@
+/*
+ * 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A text annotation with a set of attributes.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class Annotation {
+ private static final Map<String, AttributeValue> EMPTY_ATTRIBUTES =
+ Collections.unmodifiableMap(Collections.<String, AttributeValue>emptyMap());
+
+ /**
+ * Returns a new {@code Annotation} with the given description.
+ *
+ * @param description the text description of the {@code Annotation}.
+ * @return a new {@code Annotation} with the given description.
+ * @throws NullPointerException if {@code description} is {@code null}.
+ * @since 0.5
+ */
+ public static Annotation fromDescription(String description) {
+ return new AutoValue_Annotation(description, EMPTY_ATTRIBUTES);
+ }
+
+ /**
+ * Returns a new {@code Annotation} with the given description and set of attributes.
+ *
+ * @param description the text description of the {@code Annotation}.
+ * @param attributes the attributes of the {@code Annotation}.
+ * @return a new {@code Annotation} with the given description and set of attributes.
+ * @throws NullPointerException if {@code description} or {@code attributes} are {@code null}.
+ * @since 0.5
+ */
+ public static Annotation fromDescriptionAndAttributes(
+ String description, Map<String, AttributeValue> attributes) {
+ return new AutoValue_Annotation(
+ description,
+ Collections.unmodifiableMap(
+ new HashMap<String, AttributeValue>(Utils.checkNotNull(attributes, "attributes"))));
+ }
+
+ /**
+ * Return the description of the {@code Annotation}.
+ *
+ * @return the description of the {@code Annotation}.
+ * @since 0.5
+ */
+ public abstract String getDescription();
+
+ /**
+ * Return the attributes of the {@code Annotation}.
+ *
+ * @return the attributes of the {@code Annotation}.
+ * @since 0.5
+ */
+ public abstract Map<String, AttributeValue> getAttributes();
+
+ Annotation() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/AttributeValue.java b/api/src/main/java/io/opencensus/trace/AttributeValue.java
new file mode 100644
index 00000000..efa9d1df
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/AttributeValue.java
@@ -0,0 +1,255 @@
+/*
+ * 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Function;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents all the possible values for an attribute. An attribute can have 3 types
+ * of values: {@code String}, {@code Boolean} or {@code Long}.
+ *
+ * @since 0.5
+ */
+@Immutable
+public abstract class AttributeValue {
+ /**
+ * Returns an {@code AttributeValue} with a string value.
+ *
+ * @param stringValue The new value.
+ * @return an {@code AttributeValue} with a string value.
+ * @throws NullPointerException if {@code stringValue} is {@code null}.
+ * @since 0.5
+ */
+ public static AttributeValue stringAttributeValue(String stringValue) {
+ return AttributeValueString.create(stringValue);
+ }
+
+ /**
+ * Returns an {@code AttributeValue} with a boolean value.
+ *
+ * @param booleanValue The new value.
+ * @return an {@code AttributeValue} with a boolean value.
+ * @since 0.5
+ */
+ public static AttributeValue booleanAttributeValue(boolean booleanValue) {
+ return AttributeValueBoolean.create(booleanValue);
+ }
+
+ /**
+ * Returns an {@code AttributeValue} with a long value.
+ *
+ * @param longValue The new value.
+ * @return an {@code AttributeValue} with a long value.
+ * @since 0.5
+ */
+ public static AttributeValue longAttributeValue(long longValue) {
+ return AttributeValueLong.create(longValue);
+ }
+
+ /**
+ * Returns an {@code AttributeValue} with a double value.
+ *
+ * @param doubleValue The new value.
+ * @return an {@code AttributeValue} with a double value.
+ * @since 0.17
+ */
+ public static AttributeValue doubleAttributeValue(double doubleValue) {
+ return AttributeValueDouble.create(doubleValue);
+ }
+
+ AttributeValue() {}
+
+ /**
+ * Applies a function to the underlying value. The function that is called depends on the value's
+ * type, which can be {@code String}, {@code Long}, or {@code Boolean}.
+ *
+ * @param stringFunction the function that should be applied if the value has type {@code String}.
+ * @param longFunction the function that should be applied if the value has type {@code Long}.
+ * @param booleanFunction the function that should be applied if the value has type {@code
+ * Boolean}.
+ * @param defaultFunction the function that should be applied if the value has a type that was
+ * added after this {@code match} method was added to the API. See {@link
+ * io.opencensus.common.Functions} for some common functions for handling unknown types.
+ * @return the result of the function applied to the underlying value.
+ * @since 0.5
+ * @deprecated in favor of {@link #match(Function, Function, Function, Function, Function)}.
+ */
+ @Deprecated
+ public abstract <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<Object, T> defaultFunction);
+
+ /**
+ * Applies a function to the underlying value. The function that is called depends on the value's
+ * type, which can be {@code String}, {@code Long}, or {@code Boolean}.
+ *
+ * @param stringFunction the function that should be applied if the value has type {@code String}.
+ * @param longFunction the function that should be applied if the value has type {@code Long}.
+ * @param booleanFunction the function that should be applied if the value has type {@code
+ * Boolean}.
+ * @param doubleFunction the function that should be applied if the value has type {@code Double}.
+ * @param defaultFunction the function that should be applied if the value has a type that was
+ * added after this {@code match} method was added to the API. See {@link
+ * io.opencensus.common.Functions} for some common functions for handling unknown types.
+ * @return the result of the function applied to the underlying value.
+ * @since 0.17
+ */
+ @SuppressWarnings("InconsistentOverloads")
+ public abstract <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Double, T> doubleFunction,
+ Function<Object, T> defaultFunction);
+
+ @Immutable
+ @AutoValue
+ abstract static class AttributeValueString extends AttributeValue {
+
+ AttributeValueString() {}
+
+ static AttributeValue create(String stringValue) {
+ return new AutoValue_AttributeValue_AttributeValueString(
+ Utils.checkNotNull(stringValue, "stringValue"));
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<Object, T> defaultFunction) {
+ return stringFunction.apply(getStringValue());
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Double, T> doubleFunction,
+ Function<Object, T> defaultFunction) {
+ return stringFunction.apply(getStringValue());
+ }
+
+ abstract String getStringValue();
+ }
+
+ @Immutable
+ @AutoValue
+ abstract static class AttributeValueBoolean extends AttributeValue {
+
+ AttributeValueBoolean() {}
+
+ static AttributeValue create(Boolean booleanValue) {
+ return new AutoValue_AttributeValue_AttributeValueBoolean(
+ Utils.checkNotNull(booleanValue, "booleanValue"));
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<Object, T> defaultFunction) {
+ return booleanFunction.apply(getBooleanValue());
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Double, T> doubleFunction,
+ Function<Object, T> defaultFunction) {
+ return booleanFunction.apply(getBooleanValue());
+ }
+
+ abstract Boolean getBooleanValue();
+ }
+
+ @Immutable
+ @AutoValue
+ abstract static class AttributeValueLong extends AttributeValue {
+
+ AttributeValueLong() {}
+
+ static AttributeValue create(Long longValue) {
+ return new AutoValue_AttributeValue_AttributeValueLong(
+ Utils.checkNotNull(longValue, "longValue"));
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<Object, T> defaultFunction) {
+ return longFunction.apply(getLongValue());
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Double, T> doubleFunction,
+ Function<Object, T> defaultFunction) {
+ return longFunction.apply(getLongValue());
+ }
+
+ abstract Long getLongValue();
+ }
+
+ @Immutable
+ @AutoValue
+ abstract static class AttributeValueDouble extends AttributeValue {
+
+ AttributeValueDouble() {}
+
+ static AttributeValue create(Double doubleValue) {
+ return new AutoValue_AttributeValue_AttributeValueDouble(
+ Utils.checkNotNull(doubleValue, "doubleValue"));
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<Object, T> defaultFunction) {
+ return defaultFunction.apply(getDoubleValue());
+ }
+
+ @Override
+ public final <T> T match(
+ Function<? super String, T> stringFunction,
+ Function<? super Boolean, T> booleanFunction,
+ Function<? super Long, T> longFunction,
+ Function<? super Double, T> doubleFunction,
+ Function<Object, T> defaultFunction) {
+ return doubleFunction.apply(getDoubleValue());
+ }
+
+ abstract Double getDoubleValue();
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/BaseMessageEvent.java b/api/src/main/java/io/opencensus/trace/BaseMessageEvent.java
new file mode 100644
index 00000000..5ad961f6
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/BaseMessageEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018, 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.trace;
+
+/**
+ * Superclass for {@link MessageEvent} and {@link NetworkEvent} to resolve API backward
+ * compatibility issue.
+ *
+ * <p>{@code SpanData.create} can't be overloaded with parameter types that differ only in the type
+ * of the TimedEvent, because the signatures are the same after generic type erasure. {@code
+ * BaseMessageEvent} allows the same method to accept both {@code TimedEvents<NetworkEvent>} and
+ * {@code TimedEvents<MessageEvent>}.
+ *
+ * <p>This class should only be extended by {@code NetworkEvent} and {@code MessageEvent}.
+ *
+ * @deprecated This class is for internal use only.
+ * @since 0.12
+ */
+@Deprecated
+public abstract class BaseMessageEvent {
+ // package protected to avoid users to extend it.
+ BaseMessageEvent() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/BlankSpan.java b/api/src/main/java/io/opencensus/trace/BlankSpan.java
new file mode 100644
index 00000000..af6456d3
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/BlankSpan.java
@@ -0,0 +1,102 @@
+/*
+ * 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * The {@code BlankSpan} is a singleton class, which is the default {@link Span} that is used when
+ * no {@code Span} implementation is available. All operations are no-op.
+ *
+ * <p>Used also to stop tracing, see {@link Tracer#withSpan}.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class BlankSpan extends Span {
+ /**
+ * Singleton instance of this class.
+ *
+ * @since 0.5
+ */
+ public static final BlankSpan INSTANCE = new BlankSpan();
+
+ private BlankSpan() {
+ super(SpanContext.INVALID, null);
+ }
+
+ /** No-op implementation of the {@link Span#putAttribute(String, AttributeValue)} method. */
+ @Override
+ public void putAttribute(String key, AttributeValue value) {
+ Utils.checkNotNull(key, "key");
+ Utils.checkNotNull(value, "value");
+ }
+
+ /** No-op implementation of the {@link Span#putAttributes(Map)} method. */
+ @Override
+ public void putAttributes(Map<String, AttributeValue> attributes) {
+ Utils.checkNotNull(attributes, "attributes");
+ }
+
+ /** No-op implementation of the {@link Span#addAnnotation(String, Map)} method. */
+ @Override
+ public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+ Utils.checkNotNull(description, "description");
+ Utils.checkNotNull(attributes, "attributes");
+ }
+
+ /** No-op implementation of the {@link Span#addAnnotation(Annotation)} method. */
+ @Override
+ public void addAnnotation(Annotation annotation) {
+ Utils.checkNotNull(annotation, "annotation");
+ }
+
+ /** No-op implementation of the {@link Span#addNetworkEvent(NetworkEvent)} method. */
+ @Override
+ @Deprecated
+ public void addNetworkEvent(NetworkEvent networkEvent) {}
+
+ /** No-op implementation of the {@link Span#addMessageEvent(MessageEvent)} method. */
+ @Override
+ public void addMessageEvent(MessageEvent messageEvent) {
+ Utils.checkNotNull(messageEvent, "messageEvent");
+ }
+
+ /** No-op implementation of the {@link Span#addLink(Link)} method. */
+ @Override
+ public void addLink(Link link) {
+ Utils.checkNotNull(link, "link");
+ }
+
+ @Override
+ public void setStatus(Status status) {
+ Utils.checkNotNull(status, "status");
+ }
+
+ /** No-op implementation of the {@link Span#end(EndSpanOptions)} method. */
+ @Override
+ public void end(EndSpanOptions options) {
+ Utils.checkNotNull(options, "options");
+ }
+
+ @Override
+ public String toString() {
+ return "BlankSpan";
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/CurrentSpanUtils.java b/api/src/main/java/io/opencensus/trace/CurrentSpanUtils.java
new file mode 100644
index 00000000..aa2f055a
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/CurrentSpanUtils.java
@@ -0,0 +1,180 @@
+/*
+ * 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.trace;
+
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.unsafe.ContextUtils;
+import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
+
+/** Util methods/functionality to interact with the {@link Span} in the {@link io.grpc.Context}. */
+final class CurrentSpanUtils {
+ // No instance of this class.
+ private CurrentSpanUtils() {}
+
+ /**
+ * Returns The {@link Span} from the current context.
+ *
+ * @return The {@code Span} from the current context.
+ */
+ @Nullable
+ static Span getCurrentSpan() {
+ return ContextUtils.CONTEXT_SPAN_KEY.get();
+ }
+
+ /**
+ * Enters the scope of code where the given {@link Span} is in the current context, and returns an
+ * object that represents that scope. The scope is exited when the returned object is closed.
+ *
+ * <p>Supports try-with-resource idiom.
+ *
+ * @param span The {@code Span} to be set to the current context.
+ * @param endSpan if {@code true} the returned {@code Scope} will close the {@code Span}.
+ * @return An object that defines a scope where the given {@code Span} is set to the current
+ * context.
+ */
+ static Scope withSpan(Span span, boolean endSpan) {
+ return new ScopeInSpan(span, endSpan);
+ }
+
+ /**
+ * Wraps a {@link Runnable} so that it executes with the {@code span} as the current {@code Span}.
+ *
+ * @param span the {@code Span} to be set as current.
+ * @param endSpan if {@code true} the returned {@code Runnable} will close the {@code Span}.
+ * @param runnable the {@code Runnable} to run in the {@code Span}.
+ * @return the wrapped {@code Runnable}.
+ */
+ static Runnable withSpan(Span span, boolean endSpan, Runnable runnable) {
+ return new RunnableInSpan(span, runnable, endSpan);
+ }
+
+ /**
+ * Wraps a {@link Callable} so that it executes with the {@code span} as the current {@code Span}.
+ *
+ * @param span the {@code Span} to be set as current.
+ * @param endSpan if {@code true} the returned {@code Runnable} will close the {@code Span}.
+ * @param callable the {@code Callable} to run in the {@code Span}.
+ * @return the wrapped {@code Callable}.
+ */
+ static <C> Callable<C> withSpan(Span span, boolean endSpan, Callable<C> callable) {
+ return new CallableInSpan<C>(span, callable, endSpan);
+ }
+
+ // Defines an arbitrary scope of code as a traceable operation. Supports try-with-resources idiom.
+ private static final class ScopeInSpan implements Scope {
+ private final Context origContext;
+ private final Span span;
+ private final boolean endSpan;
+
+ /**
+ * Constructs a new {@link ScopeInSpan}.
+ *
+ * @param span is the {@code Span} to be added to the current {@code io.grpc.Context}.
+ */
+ private ScopeInSpan(Span span, boolean endSpan) {
+ this.span = span;
+ this.endSpan = endSpan;
+ origContext = Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, span).attach();
+ }
+
+ @Override
+ public void close() {
+ Context.current().detach(origContext);
+ if (endSpan) {
+ span.end();
+ }
+ }
+ }
+
+ private static final class RunnableInSpan implements Runnable {
+ // TODO(bdrutu): Investigate if the extra private visibility increases the generated bytecode.
+ private final Span span;
+ private final Runnable runnable;
+ private final boolean endSpan;
+
+ private RunnableInSpan(Span span, Runnable runnable, boolean endSpan) {
+ this.span = span;
+ this.runnable = runnable;
+ this.endSpan = endSpan;
+ }
+
+ @Override
+ public void run() {
+ Context origContext =
+ Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, span).attach();
+ try {
+ runnable.run();
+ } catch (Throwable t) {
+ setErrorStatus(span, t);
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ } else if (t instanceof Error) {
+ throw (Error) t;
+ }
+ throw new RuntimeException("unexpected", t);
+ } finally {
+ Context.current().detach(origContext);
+ if (endSpan) {
+ span.end();
+ }
+ }
+ }
+ }
+
+ private static final class CallableInSpan<V> implements Callable<V> {
+ private final Span span;
+ private final Callable<V> callable;
+ private final boolean endSpan;
+
+ private CallableInSpan(Span span, Callable<V> callable, boolean endSpan) {
+ this.span = span;
+ this.callable = callable;
+ this.endSpan = endSpan;
+ }
+
+ @Override
+ public V call() throws Exception {
+ Context origContext =
+ Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, span).attach();
+ try {
+ return callable.call();
+ } catch (Exception e) {
+ setErrorStatus(span, e);
+ throw e;
+ } catch (Throwable t) {
+ setErrorStatus(span, t);
+ if (t instanceof Error) {
+ throw (Error) t;
+ }
+ throw new RuntimeException("unexpected", t);
+ } finally {
+ Context.current().detach(origContext);
+ if (endSpan) {
+ span.end();
+ }
+ }
+ }
+ }
+
+ private static void setErrorStatus(Span span, Throwable t) {
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage()));
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/EndSpanOptions.java b/api/src/main/java/io/opencensus/trace/EndSpanOptions.java
new file mode 100644
index 00000000..b0d9a470
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/EndSpanOptions.java
@@ -0,0 +1,127 @@
+/*
+ * 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import java.util.Collection;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that enables overriding the default values used when ending a {@link Span}. Allows
+ * overriding the {@link Status status}.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class EndSpanOptions {
+ /**
+ * The default {@code EndSpanOptions}.
+ *
+ * @since 0.5
+ */
+ public static final EndSpanOptions DEFAULT = builder().build();
+
+ /**
+ * Returns a new {@link Builder} with default options.
+ *
+ * @return a new {@code Builder} with default options.
+ * @since 0.5
+ */
+ public static Builder builder() {
+ return new AutoValue_EndSpanOptions.Builder().setSampleToLocalSpanStore(false);
+ }
+
+ /**
+ * If {@code true} this is equivalent with calling the {@link
+ * io.opencensus.trace.export.SampledSpanStore#registerSpanNamesForCollection(Collection)} in
+ * advance for this span name.
+ *
+ * <p>It is strongly recommended to use the {@link
+ * io.opencensus.trace.export.SampledSpanStore#registerSpanNamesForCollection(Collection)} API
+ * instead.
+ *
+ * @return {@code true} if the name of the {@code Span} should be registered to the {@code
+ * io.opencensus.trace.export.SampledSpanStore}.
+ * @since 0.8
+ */
+ @ExperimentalApi
+ public abstract boolean getSampleToLocalSpanStore();
+
+ /**
+ * Returns the status.
+ *
+ * <p>If {@code null} then the {@link Span} will record the {@link Status} set via {@link
+ * Span#setStatus(Status)} or the default {@link Status#OK} if no status was set.
+ *
+ * @return the status.
+ * @since 0.5
+ */
+ @Nullable
+ public abstract Status getStatus();
+
+ /**
+ * Builder class for {@link EndSpanOptions}.
+ *
+ * @since 0.5
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ /**
+ * Sets the status for the {@link Span}.
+ *
+ * <p>If set, this will override the status set via {@link Span#setStatus(Status)}.
+ *
+ * @param status the status.
+ * @return this.
+ * @since 0.5
+ */
+ public abstract Builder setStatus(Status status);
+
+ /**
+ * If set to {@code true} this is equivalent with calling the {@link
+ * io.opencensus.trace.export.SampledSpanStore#registerSpanNamesForCollection(Collection)} in
+ * advance for the given span name.
+ *
+ * <p>WARNING: setting this option to a randomly generated span name can OOM your process
+ * because the library will save samples for each name.
+ *
+ * <p>It is strongly recommended to use the {@link
+ * io.opencensus.trace.export.SampledSpanStore#registerSpanNamesForCollection(Collection)} API
+ * instead.
+ *
+ * @return this.
+ * @since 0.8
+ */
+ @ExperimentalApi
+ public abstract Builder setSampleToLocalSpanStore(boolean sampleToLocalSpanStore);
+
+ /**
+ * Builds and returns a {@code EndSpanOptions} with the desired settings.
+ *
+ * @return a {@code EndSpanOptions} with the desired settings.
+ * @since 0.5
+ */
+ public abstract EndSpanOptions build();
+
+ Builder() {}
+ }
+
+ EndSpanOptions() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/Link.java b/api/src/main/java/io/opencensus/trace/Link.java
new file mode 100644
index 00000000..1de79710
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Link.java
@@ -0,0 +1,124 @@
+/*
+ * 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.trace;
+
+import com.google.auto.value.AutoValue;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A link to a {@link Span} from a different trace.
+ *
+ * <p>It requires a {@link Type} which describes the relationship with the linked {@code Span} and
+ * the identifiers of the linked {@code Span}.
+ *
+ * <p>Used (for example) in batching operations, where a single batch handler processes multiple
+ * requests from different traces.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class Link {
+ private static final Map<String, AttributeValue> EMPTY_ATTRIBUTES = Collections.emptyMap();
+
+ /**
+ * The relationship with the linked {@code Span} relative to the current {@code Span}.
+ *
+ * @since 0.5
+ */
+ public enum Type {
+ /**
+ * When the linked {@code Span} is a child of the current {@code Span}.
+ *
+ * @since 0.5
+ */
+ CHILD_LINKED_SPAN,
+ /**
+ * When the linked {@code Span} is a parent of the current {@code Span}.
+ *
+ * @since 0.5
+ */
+ PARENT_LINKED_SPAN
+ }
+
+ /**
+ * Returns a new {@code Link}.
+ *
+ * @param context the context of the linked {@code Span}.
+ * @param type the type of the relationship with the linked {@code Span}.
+ * @return a new {@code Link}.
+ * @since 0.5
+ */
+ public static Link fromSpanContext(SpanContext context, Type type) {
+ return new AutoValue_Link(context.getTraceId(), context.getSpanId(), type, EMPTY_ATTRIBUTES);
+ }
+
+ /**
+ * Returns a new {@code Link}.
+ *
+ * @param context the context of the linked {@code Span}.
+ * @param type the type of the relationship with the linked {@code Span}.
+ * @param attributes the attributes of the {@code Link}.
+ * @return a new {@code Link}.
+ * @since 0.5
+ */
+ public static Link fromSpanContext(
+ SpanContext context, Type type, Map<String, AttributeValue> attributes) {
+ return new AutoValue_Link(
+ context.getTraceId(),
+ context.getSpanId(),
+ type,
+ Collections.unmodifiableMap(new HashMap<String, AttributeValue>(attributes)));
+ }
+
+ /**
+ * Returns the {@code TraceId}.
+ *
+ * @return the {@code TraceId}.
+ * @since 0.5
+ */
+ public abstract TraceId getTraceId();
+
+ /**
+ * Returns the {@code SpanId}.
+ *
+ * @return the {@code SpanId}
+ * @since 0.5
+ */
+ public abstract SpanId getSpanId();
+
+ /**
+ * Returns the {@code Type}.
+ *
+ * @return the {@code Type}.
+ * @since 0.5
+ */
+ public abstract Type getType();
+
+ /**
+ * Returns the set of attributes.
+ *
+ * @return the set of attributes.
+ * @since 0.5
+ */
+ public abstract Map<String, AttributeValue> getAttributes();
+
+ Link() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/LowerCaseBase16Encoding.java b/api/src/main/java/io/opencensus/trace/LowerCaseBase16Encoding.java
new file mode 100644
index 00000000..bca95868
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/LowerCaseBase16Encoding.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.Arrays;
+
+/** Internal copy of the Guava implementation of the {@code BaseEncoding.base16().lowerCase()}. */
+final class LowerCaseBase16Encoding {
+ private static final String ALPHABET = "0123456789abcdef";
+ private static final int ASCII_CHARACTERS = 128;
+ private static final char[] ENCODING = buildEncodingArray();
+ private static final byte[] DECODING = buildDecodingArray();
+
+ private static char[] buildEncodingArray() {
+ char[] encoding = new char[512];
+ for (int i = 0; i < 256; ++i) {
+ encoding[i] = ALPHABET.charAt(i >>> 4);
+ encoding[i | 0x100] = ALPHABET.charAt(i & 0xF);
+ }
+ return encoding;
+ }
+
+ private static byte[] buildDecodingArray() {
+ byte[] decoding = new byte[ASCII_CHARACTERS];
+ Arrays.fill(decoding, (byte) -1);
+ for (int i = 0; i < ALPHABET.length(); i++) {
+ char c = ALPHABET.charAt(i);
+ decoding[c] = (byte) i;
+ }
+ return decoding;
+ }
+
+ /**
+ * Encodes the specified byte array, and returns the encoded {@code String}.
+ *
+ * @param bytes byte array to be encoded.
+ * @return the encoded {@code String}.
+ */
+ static String encodeToString(byte[] bytes) {
+ StringBuilder stringBuilder = new StringBuilder(bytes.length * 2);
+ for (byte byteVal : bytes) {
+ int b = byteVal & 0xFF;
+ stringBuilder.append(ENCODING[b]);
+ stringBuilder.append(ENCODING[b | 0x100]);
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Decodes the specified character sequence, and returns the resulting {@code byte[]}.
+ *
+ * @param chars the character sequence to be decoded.
+ * @return the resulting {@code byte[]}
+ * @throws IllegalArgumentException if the input is not a valid encoded string according to this
+ * encoding.
+ */
+ static byte[] decodeToBytes(CharSequence chars) {
+ Utils.checkArgument(chars.length() % 2 == 0, "Invalid input length " + chars.length());
+ int bytesWritten = 0;
+ byte[] bytes = new byte[chars.length() / 2];
+ for (int i = 0; i < chars.length(); i += 2) {
+ bytes[bytesWritten++] = decodeByte(chars.charAt(i), chars.charAt(i + 1));
+ }
+ return bytes;
+ }
+
+ private static byte decodeByte(char hi, char lo) {
+ Utils.checkArgument(lo < ASCII_CHARACTERS && DECODING[lo] != -1, "Invalid character " + lo);
+ Utils.checkArgument(hi < ASCII_CHARACTERS && DECODING[hi] != -1, "Invalid character " + hi);
+ int decoded = DECODING[hi] << 4 | DECODING[lo];
+ return (byte) decoded;
+ }
+
+ // Private constructor to disallow instances.
+ private LowerCaseBase16Encoding() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/MessageEvent.java b/api/src/main/java/io/opencensus/trace/MessageEvent.java
new file mode 100644
index 00000000..4b693aaa
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/MessageEvent.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a generic messaging event. This class can represent messaging happened in
+ * any layer, especially higher application layer. Thus, it can be used when recording events in
+ * pipeline works, in-process bidirectional streams and batch processing.
+ *
+ * <p>It requires a {@link Type type} and a message id that serves to uniquely identify each
+ * message. It can optionally have information about the message size.
+ *
+ * @since 0.12
+ */
+@Immutable
+@AutoValue
+@SuppressWarnings("deprecation")
+public abstract class MessageEvent extends BaseMessageEvent {
+ /**
+ * Available types for a {@code MessageEvent}.
+ *
+ * @since 0.12
+ */
+ public enum Type {
+ /**
+ * When the message was sent.
+ *
+ * @since 0.12
+ */
+ SENT,
+ /**
+ * When the message was received.
+ *
+ * @since 0.12
+ */
+ RECEIVED,
+ }
+
+ /**
+ * Returns a new {@link Builder} with default values.
+ *
+ * @param type designates whether this is a send or receive message.
+ * @param messageId serves to uniquely identify each message.
+ * @return a new {@code Builder} with default values.
+ * @throws NullPointerException if {@code type} is {@code null}.
+ * @since 0.12
+ */
+ public static Builder builder(Type type, long messageId) {
+ return new AutoValue_MessageEvent.Builder()
+ .setType(Utils.checkNotNull(type, "type"))
+ .setMessageId(messageId)
+ // We need to set a value for the message size because the autovalue requires all
+ // primitives to be initialized.
+ .setUncompressedMessageSize(0)
+ .setCompressedMessageSize(0);
+ }
+
+ /**
+ * Returns the type of the {@code MessageEvent}.
+ *
+ * @return the type of the {@code MessageEvent}.
+ * @since 0.12
+ */
+ public abstract Type getType();
+
+ /**
+ * Returns the message id argument that serves to uniquely identify each message.
+ *
+ * @return the message id of the {@code MessageEvent}.
+ * @since 0.12
+ */
+ public abstract long getMessageId();
+
+ /**
+ * Returns the uncompressed size in bytes of the {@code MessageEvent}.
+ *
+ * @return the uncompressed size in bytes of the {@code MessageEvent}.
+ * @since 0.12
+ */
+ public abstract long getUncompressedMessageSize();
+
+ /**
+ * Returns the compressed size in bytes of the {@code MessageEvent}.
+ *
+ * @return the compressed size in bytes of the {@code MessageEvent}.
+ * @since 0.12
+ */
+ public abstract long getCompressedMessageSize();
+
+ /**
+ * Builder class for {@link MessageEvent}.
+ *
+ * @since 0.12
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ // Package protected methods because these values are mandatory and set only in the
+ // MessageEvent#builder() function.
+ abstract Builder setType(Type type);
+
+ abstract Builder setMessageId(long messageId);
+
+ /**
+ * Sets the uncompressed message size.
+ *
+ * @param uncompressedMessageSize represents the uncompressed size in bytes of this message.
+ * @return this.
+ * @since 0.12
+ */
+ public abstract Builder setUncompressedMessageSize(long uncompressedMessageSize);
+
+ /**
+ * Sets the compressed message size.
+ *
+ * @param compressedMessageSize represents the compressed size in bytes of this message.
+ * @return this.
+ * @since 0.12
+ */
+ public abstract Builder setCompressedMessageSize(long compressedMessageSize);
+
+ /**
+ * Builds and returns a {@code MessageEvent} with the desired values.
+ *
+ * @return a {@code MessageEvent} with the desired values.
+ * @since 0.12
+ */
+ public abstract MessageEvent build();
+
+ Builder() {}
+ }
+
+ MessageEvent() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/NetworkEvent.java b/api/src/main/java/io/opencensus/trace/NetworkEvent.java
new file mode 100644
index 00000000..722029e5
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/NetworkEvent.java
@@ -0,0 +1,200 @@
+/*
+ * 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a network event. It requires a {@link Type type} and a message id that
+ * serves to uniquely identify each network message. It can optionally can have information about
+ * the kernel time and message size.
+ *
+ * @deprecated Use {@link MessageEvent}.
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+@AutoValue.CopyAnnotations
+@Deprecated
+public abstract class NetworkEvent extends io.opencensus.trace.BaseMessageEvent {
+ /**
+ * Available types for a {@code NetworkEvent}.
+ *
+ * @since 0.5
+ */
+ public enum Type {
+ /**
+ * When the message was sent.
+ *
+ * @since 0.5
+ */
+ SENT,
+ /**
+ * When the message was received.
+ *
+ * @since 0.5
+ */
+ RECV,
+ }
+
+ /**
+ * Returns a new {@link Builder} with default values.
+ *
+ * @param type designates whether this is a network send or receive message.
+ * @param messageId serves to uniquely identify each network message.
+ * @return a new {@code Builder} with default values.
+ * @throws NullPointerException if {@code type} is {@code null}.
+ * @since 0.5
+ */
+ public static Builder builder(Type type, long messageId) {
+ return new AutoValue_NetworkEvent.Builder()
+ .setType(Utils.checkNotNull(type, "type"))
+ .setMessageId(messageId)
+ // We need to set a value for the message size because the autovalue requires all
+ // primitives to be initialized.
+ .setUncompressedMessageSize(0)
+ .setCompressedMessageSize(0);
+ }
+
+ /**
+ * Returns the kernel timestamp associated with the {@code NetworkEvent} or {@code null} if not
+ * set.
+ *
+ * @return the kernel timestamp associated with the {@code NetworkEvent} or {@code null} if not
+ * set.
+ * @since 0.5
+ */
+ @Nullable
+ public abstract Timestamp getKernelTimestamp();
+
+ /**
+ * Returns the type of the {@code NetworkEvent}.
+ *
+ * @return the type of the {@code NetworkEvent}.
+ * @since 0.5
+ */
+ public abstract Type getType();
+
+ /**
+ * Returns the message id argument that serves to uniquely identify each network message.
+ *
+ * @return the message id of the {@code NetworkEvent}.
+ * @since 0.5
+ */
+ public abstract long getMessageId();
+
+ /**
+ * Returns the uncompressed size in bytes of the {@code NetworkEvent}.
+ *
+ * @return the uncompressed size in bytes of the {@code NetworkEvent}.
+ * @since 0.6
+ */
+ public abstract long getUncompressedMessageSize();
+
+ /**
+ * Returns the compressed size in bytes of the {@code NetworkEvent}.
+ *
+ * @return the compressed size in bytes of the {@code NetworkEvent}.
+ * @since 0.6
+ */
+ public abstract long getCompressedMessageSize();
+
+ /**
+ * Returns the uncompressed size in bytes of the {@code NetworkEvent}.
+ *
+ * @deprecated Use {@link #getUncompressedMessageSize}.
+ * @return the uncompressed size in bytes of the {@code NetworkEvent}.
+ * @since 0.5
+ */
+ @Deprecated
+ public long getMessageSize() {
+ return getUncompressedMessageSize();
+ }
+
+ /**
+ * Builder class for {@link NetworkEvent}.
+ *
+ * @deprecated {@link NetworkEvent} is deprecated. Please use {@link MessageEvent} and its builder
+ * {@link MessageEvent.Builder}.
+ * @since 0.5
+ */
+ @AutoValue.Builder
+ @Deprecated
+ public abstract static class Builder {
+ // Package protected methods because these values are mandatory and set only in the
+ // NetworkEvent#builder() function.
+ abstract Builder setType(Type type);
+
+ abstract Builder setMessageId(long messageId);
+
+ /**
+ * Sets the kernel timestamp.
+ *
+ * @param kernelTimestamp The kernel timestamp of the event.
+ * @return this.
+ * @since 0.5
+ */
+ public abstract Builder setKernelTimestamp(@Nullable Timestamp kernelTimestamp);
+
+ /**
+ * Sets the uncompressed message size.
+ *
+ * @deprecated Use {@link #setUncompressedMessageSize}.
+ * @param messageSize represents the uncompressed size in bytes of this message.
+ * @return this.
+ * @since 0.5
+ */
+ @Deprecated
+ public Builder setMessageSize(long messageSize) {
+ return setUncompressedMessageSize(messageSize);
+ }
+
+ /**
+ * Sets the uncompressed message size.
+ *
+ * @param uncompressedMessageSize represents the uncompressed size in bytes of this message.
+ * @return this.
+ * @since 0.6
+ */
+ public abstract Builder setUncompressedMessageSize(long uncompressedMessageSize);
+
+ /**
+ * Sets the compressed message size.
+ *
+ * @param compressedMessageSize represents the compressed size in bytes of this message.
+ * @return this.
+ * @since 0.6
+ */
+ public abstract Builder setCompressedMessageSize(long compressedMessageSize);
+
+ /**
+ * Builds and returns a {@code NetworkEvent} with the desired values.
+ *
+ * @return a {@code NetworkEvent} with the desired values.
+ * @since 0.5
+ */
+ public abstract NetworkEvent build();
+
+ Builder() {}
+ }
+
+ NetworkEvent() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/Sampler.java b/api/src/main/java/io/opencensus/trace/Sampler.java
new file mode 100644
index 00000000..e89af89b
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Sampler.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.trace;
+
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Sampler is used to make decisions on {@link Span} sampling.
+ *
+ * @since 0.5
+ */
+public abstract class Sampler {
+ /**
+ * Called during {@link Span} creation to make a sampling decision.
+ *
+ * @param parentContext the parent span's {@link SpanContext}. {@code null} if this is a root
+ * span.
+ * @param hasRemoteParent {@code true} if the parent {@code Span} is remote. {@code null} if this
+ * is a root span.
+ * @param traceId the {@link TraceId} for the new {@code Span}. This will be identical to that in
+ * the parentContext, unless this is a root span.
+ * @param spanId the {@link SpanId} for the new {@code Span}.
+ * @param name the name of the new {@code Span}.
+ * @param parentLinks the parentLinks associated with the new {@code Span}.
+ * @return {@code true} if the {@code Span} is sampled.
+ * @since 0.5
+ */
+ public abstract boolean shouldSample(
+ @Nullable SpanContext parentContext,
+ @Nullable Boolean hasRemoteParent,
+ TraceId traceId,
+ SpanId spanId,
+ String name,
+ List<Span> parentLinks);
+
+ /**
+ * Returns the description of this {@code Sampler}. This may be displayed on debug pages or in the
+ * logs.
+ *
+ * <p>Example: "ProbabilitySampler{0.000100}"
+ *
+ * @return the description of this {@code Sampler}.
+ * @since 0.6
+ */
+ public abstract String getDescription();
+}
diff --git a/api/src/main/java/io/opencensus/trace/Span.java b/api/src/main/java/io/opencensus/trace/Span.java
new file mode 100644
index 00000000..8f8253b4
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Span.java
@@ -0,0 +1,288 @@
+/*
+ * 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.trace;
+
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.internal.BaseMessageEventUtils;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/**
+ * An abstract class that represents a span. It has an associated {@link SpanContext} and a set of
+ * {@link Options}.
+ *
+ * <p>Spans are created by the {@link SpanBuilder#startSpan} method.
+ *
+ * <p>{@code Span} <b>must</b> be ended by calling {@link #end()} or {@link #end(EndSpanOptions)}
+ *
+ * @since 0.5
+ */
+public abstract class Span {
+ private static final Map<String, AttributeValue> EMPTY_ATTRIBUTES = Collections.emptyMap();
+
+ // Contains the identifiers associated with this Span.
+ private final SpanContext context;
+
+ // Contains the options associated with this Span. This object is immutable.
+ private final Set<Options> options;
+
+ /**
+ * {@code Span} options. These options are NOT propagated to child spans. These options determine
+ * features such as whether a {@code Span} should record any annotations or events.
+ *
+ * @since 0.5
+ */
+ public enum Options {
+ /**
+ * This option is set if the Span is part of a sampled distributed trace OR {@link
+ * SpanBuilder#setRecordEvents(boolean)} was called with true.
+ *
+ * @since 0.5
+ */
+ RECORD_EVENTS;
+ }
+
+ private static final Set<Options> DEFAULT_OPTIONS =
+ Collections.unmodifiableSet(EnumSet.noneOf(Options.class));
+
+ /**
+ * Creates a new {@code Span}.
+ *
+ * @param context the context associated with this {@code Span}.
+ * @param options the options associated with this {@code Span}. If {@code null} then default
+ * options will be set.
+ * @throws NullPointerException if context is {@code null}.
+ * @throws IllegalArgumentException if the {@code SpanContext} is sampled but no RECORD_EVENTS
+ * options.
+ * @since 0.5
+ */
+ protected Span(SpanContext context, @Nullable EnumSet<Options> options) {
+ this.context = Utils.checkNotNull(context, "context");
+ this.options =
+ options == null
+ ? DEFAULT_OPTIONS
+ : Collections.<Options>unmodifiableSet(EnumSet.copyOf(options));
+ Utils.checkArgument(
+ !context.getTraceOptions().isSampled() || (this.options.contains(Options.RECORD_EVENTS)),
+ "Span is sampled, but does not have RECORD_EVENTS set.");
+ }
+
+ /**
+ * Sets an attribute to the {@code Span}. If the {@code Span} previously contained a mapping for
+ * the key, the old value is replaced by the specified value.
+ *
+ * @param key the key for this attribute.
+ * @param value the value for this attribute.
+ * @since 0.6
+ */
+ public void putAttribute(String key, AttributeValue value) {
+ // Not final because for performance reasons we want to override this in the implementation.
+ // Also a default implementation is needed to not break the compatibility (users may extend this
+ // for testing).
+ Utils.checkNotNull(key, "key");
+ Utils.checkNotNull(value, "value");
+ putAttributes(Collections.singletonMap(key, value));
+ }
+
+ /**
+ * Sets a set of attributes to the {@code Span}. The effect of this call is equivalent to that of
+ * calling {@link #putAttribute(String, AttributeValue)} once for each element in the specified
+ * map.
+ *
+ * @param attributes the attributes that will be added and associated with the {@code Span}.
+ * @since 0.6
+ */
+ public void putAttributes(Map<String, AttributeValue> attributes) {
+ // Not final because we want to start overriding this method from the beginning, this will
+ // allow us to remove the addAttributes faster. All implementations MUST override this method.
+ Utils.checkNotNull(attributes, "attributes");
+ addAttributes(attributes);
+ }
+
+ /**
+ * Sets a set of attributes to the {@code Span}. The effect of this call is equivalent to that of
+ * calling {@link #putAttribute(String, AttributeValue)} once for each element in the specified
+ * map.
+ *
+ * @deprecated Use {@link #putAttributes(Map)}
+ * @param attributes the attributes that will be added and associated with the {@code Span}.
+ * @since 0.5
+ */
+ @Deprecated
+ public void addAttributes(Map<String, AttributeValue> attributes) {
+ putAttributes(attributes);
+ }
+
+ /**
+ * Adds an annotation to the {@code Span}.
+ *
+ * @param description the description of the annotation time event.
+ * @since 0.5
+ */
+ public final void addAnnotation(String description) {
+ Utils.checkNotNull(description, "description");
+ addAnnotation(description, EMPTY_ATTRIBUTES);
+ }
+
+ /**
+ * Adds an annotation to the {@code Span}.
+ *
+ * @param description the description of the annotation time event.
+ * @param attributes the attributes that will be added; these are associated with this annotation,
+ * not the {@code Span} as for {@link #putAttributes(Map)}.
+ * @since 0.5
+ */
+ public abstract void addAnnotation(String description, Map<String, AttributeValue> attributes);
+
+ /**
+ * Adds an annotation to the {@code Span}.
+ *
+ * @param annotation the annotations to add.
+ * @since 0.5
+ */
+ public abstract void addAnnotation(Annotation annotation);
+
+ /**
+ * Adds a NetworkEvent to the {@code Span}.
+ *
+ * <p>This function is only intended to be used by RPC systems (either client or server), not by
+ * higher level applications.
+ *
+ * @param networkEvent the network event to add.
+ * @deprecated Use {@link #addMessageEvent}.
+ * @since 0.5
+ */
+ @Deprecated
+ public void addNetworkEvent(NetworkEvent networkEvent) {
+ addMessageEvent(BaseMessageEventUtils.asMessageEvent(networkEvent));
+ }
+
+ /**
+ * Adds a MessageEvent to the {@code Span}.
+ *
+ * <p>This function can be used by higher level applications to record messaging event.
+ *
+ * <p>This method should always be overridden by users whose API versions are larger or equal to
+ * {@code 0.12}.
+ *
+ * @param messageEvent the message to add.
+ * @since 0.12
+ */
+ public void addMessageEvent(MessageEvent messageEvent) {
+ // Default implementation by invoking addNetworkEvent() so that any existing derived classes,
+ // including implementation and the mocked ones, do not need to override this method explicitly.
+ Utils.checkNotNull(messageEvent, "messageEvent");
+ addNetworkEvent(BaseMessageEventUtils.asNetworkEvent(messageEvent));
+ }
+
+ /**
+ * Adds a {@link Link} to the {@code Span}.
+ *
+ * <p>Used (for example) in batching operations, where a single batch handler processes multiple
+ * requests from different traces.
+ *
+ * @param link the link to add.
+ * @since 0.5
+ */
+ public abstract void addLink(Link link);
+
+ /**
+ * Sets the {@link Status} to the {@code Span}.
+ *
+ * <p>If used, this will override the default {@code Span} status. Default is {@link Status#OK}.
+ *
+ * <p>Only the value of the last call will be recorded, and implementations are free to ignore
+ * previous calls. If the status is set via {@link EndSpanOptions.Builder#setStatus(Status)} that
+ * will always be the last call.
+ *
+ * @param status the {@link Status} to set.
+ * @since 0.9
+ */
+ public void setStatus(Status status) {
+ // Implemented as no-op for backwards compatibility (for example gRPC extends Span in tests).
+ // Implementation must override this method.
+ Utils.checkNotNull(status, "status");
+ }
+
+ /**
+ * Marks the end of {@code Span} execution with the given options.
+ *
+ * <p>Only the timing of the first end call for a given {@code Span} will be recorded, and
+ * implementations are free to ignore all further calls.
+ *
+ * @param options the options to be used for the end of the {@code Span}.
+ * @since 0.5
+ */
+ public abstract void end(EndSpanOptions options);
+
+ /**
+ * Marks the end of {@code Span} execution with the default options.
+ *
+ * <p>Only the timing of the first end call for a given {@code Span} will be recorded, and
+ * implementations are free to ignore all further calls.
+ *
+ * @since 0.5
+ */
+ public final void end() {
+ end(EndSpanOptions.DEFAULT);
+ }
+
+ /**
+ * Returns the {@code SpanContext} associated with this {@code Span}.
+ *
+ * @return the {@code SpanContext} associated with this {@code Span}.
+ * @since 0.5
+ */
+ public final SpanContext getContext() {
+ return context;
+ }
+
+ /**
+ * Returns the options associated with this {@code Span}.
+ *
+ * @return the options associated with this {@code Span}.
+ * @since 0.5
+ */
+ public final Set<Options> getOptions() {
+ return options;
+ }
+
+ /**
+ * Type of span. Can be used to specify additional relationships between spans in addition to a
+ * parent/child relationship.
+ *
+ * @since 0.14
+ */
+ public enum Kind {
+ /**
+ * Indicates that the span covers server-side handling of an RPC or other remote request.
+ *
+ * @since 0.14
+ */
+ SERVER,
+
+ /**
+ * Indicates that the span covers the client-side wrapper around an RPC or other remote request.
+ *
+ * @since 0.14
+ */
+ CLIENT
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/SpanBuilder.java b/api/src/main/java/io/opencensus/trace/SpanBuilder.java
new file mode 100644
index 00000000..f3a436a6
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/SpanBuilder.java
@@ -0,0 +1,356 @@
+/*
+ * 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.trace;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import io.opencensus.common.Scope;
+import io.opencensus.internal.Utils;
+import java.util.List;
+import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
+
+/**
+ * {@link SpanBuilder} is used to construct {@link Span} instances which define arbitrary scopes of
+ * code that are sampled for distributed tracing as a single atomic unit.
+ *
+ * <p>This is a simple example where all the work is being done within a single scope and a single
+ * thread and the Context is automatically propagated:
+ *
+ * <pre>{@code
+ * class MyClass {
+ * private static final Tracer tracer = Tracing.getTracer();
+ * void doWork {
+ * // Create a Span as a child of the current Span.
+ * try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
+ * tracer.getCurrentSpan().addAnnotation("my annotation");
+ * doSomeWork(); // Here the new span is in the current Context, so it can be used
+ * // implicitly anywhere down the stack.
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>There might be cases where you do not perform all the work inside one static scope and the
+ * Context is automatically propagated:
+ *
+ * <pre>{@code
+ * class MyRpcServerInterceptorListener implements RpcServerInterceptor.Listener {
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private Span mySpan;
+ *
+ * public MyRpcInterceptor() {}
+ *
+ * public void onRequest(String rpcName, Metadata metadata) {
+ * // Create a Span as a child of the remote Span.
+ * mySpan = tracer.spanBuilderWithRemoteParent(
+ * getTraceContextFromMetadata(metadata), rpcName).startSpan();
+ * }
+ *
+ * public void onExecuteHandler(ServerCallHandler serverCallHandler) {
+ * try (Scope ws = tracer.withSpan(mySpan)) {
+ * tracer.getCurrentSpan().addAnnotation("Start rpc execution.");
+ * serverCallHandler.run(); // Here the new span is in the current Context, so it can be
+ * // used implicitly anywhere down the stack.
+ * }
+ * }
+ *
+ * // Called when the RPC is canceled and guaranteed onComplete will not be called.
+ * public void onCancel() {
+ * // IMPORTANT: DO NOT forget to ended the Span here as the work is done.
+ * mySpan.end(EndSpanOptions.builder().setStatus(Status.CANCELLED));
+ * }
+ *
+ * // Called when the RPC is done and guaranteed onCancel will not be called.
+ * public void onComplete(RpcStatus rpcStatus) {
+ * // IMPORTANT: DO NOT forget to ended the Span here as the work is done.
+ * mySpan.end(EndSpanOptions.builder().setStatus(rpcStatusToCanonicalTraceStatus(status));
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>This is a simple example where all the work is being done within a single scope and the
+ * Context is manually propagated:
+ *
+ * <pre>{@code
+ * class MyClass {
+ * private static final Tracer tracer = Tracing.getTracer();
+ * void DoWork(Span parent) {
+ * Span childSpan = tracer.spanBuilderWithExplicitParent("MyChildSpan", parent).startSpan();
+ * childSpan.addAnnotation("my annotation");
+ * try {
+ * doSomeWork(childSpan); // Manually propagate the new span down the stack.
+ * } finally {
+ * // To make sure we end the span even in case of an exception.
+ * childSpan.end(); // Manually end the span.
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>If your Java version is less than Java SE 7, see {@link SpanBuilder#startSpan} and {@link
+ * SpanBuilder#startScopedSpan} for usage examples.
+ *
+ * @since 0.5
+ */
+public abstract class SpanBuilder {
+
+ /**
+ * Sets the {@link Sampler} to use. If not set, the implementation will provide a default.
+ *
+ * @param sampler the {@code Sampler} to use when determining sampling for a {@code Span}.
+ * @return this.
+ * @since 0.5
+ */
+ public abstract SpanBuilder setSampler(Sampler sampler);
+
+ /**
+ * Sets the {@code List} of parent links. Links are used to link {@link Span}s in different
+ * traces. Used (for example) in batching operations, where a single batch handler processes
+ * multiple requests from different traces.
+ *
+ * @param parentLinks new links to be added.
+ * @return this.
+ * @throws NullPointerException if {@code parentLinks} is {@code null}.
+ * @since 0.5
+ */
+ public abstract SpanBuilder setParentLinks(List<Span> parentLinks);
+
+ /**
+ * Sets the option {@link Span.Options#RECORD_EVENTS} for the newly created {@code Span}. If not
+ * called, the implementation will provide a default.
+ *
+ * @param recordEvents new value determining if this {@code Span} should have events recorded.
+ * @return this.
+ * @since 0.5
+ */
+ public abstract SpanBuilder setRecordEvents(boolean recordEvents);
+
+ /**
+ * Sets the {@link Span.Kind} for the newly created {@code Span}. If not called, the
+ * implementation will provide a default.
+ *
+ * @param spanKind the kind of the newly created {@code Span}.
+ * @return this.
+ * @since 0.14
+ */
+ public SpanBuilder setSpanKind(@Nullable Span.Kind spanKind) {
+ return this;
+ }
+
+ /**
+ * Starts a new {@link Span}.
+ *
+ * <p>Users <b>must</b> manually call {@link Span#end()} or {@link Span#end(EndSpanOptions)} to
+ * end this {@code Span}.
+ *
+ * <p>Does not install the newly created {@code Span} to the current Context.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * class MyClass {
+ * private static final Tracer tracer = Tracing.getTracer();
+ * void DoWork(Span parent) {
+ * Span childSpan = tracer.spanBuilderWithExplicitParent("MyChildSpan", parent).startSpan();
+ * childSpan.addAnnotation("my annotation");
+ * try {
+ * doSomeWork(childSpan); // Manually propagate the new span down the stack.
+ * } finally {
+ * // To make sure we end the span even in case of an exception.
+ * childSpan.end(); // Manually end the span.
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * @return the newly created {@code Span}.
+ * @since 0.5
+ */
+ public abstract Span startSpan();
+
+ /**
+ * Starts a new span and sets it as the {@link Tracer#getCurrentSpan current span}.
+ *
+ * <p>Enters the scope of code where the newly created {@code Span} is in the current Context, and
+ * returns an object that represents that scope. When the returned object is closed, the scope is
+ * exited, the previous Context is restored, and the newly created {@code Span} is ended using
+ * {@link Span#end}.
+ *
+ * <p>Supports try-with-resource idiom.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * class MyClass {
+ * private static final Tracer tracer = Tracing.getTracer();
+ * void doWork {
+ * // Create a Span as a child of the current Span.
+ * try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
+ * tracer.getCurrentSpan().addAnnotation("my annotation");
+ * doSomeWork(); // Here the new span is in the current Context, so it can be used
+ * // implicitly anywhere down the stack. Anytime in this closure the span
+ * // can be accessed via tracer.getCurrentSpan().
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Prior to Java SE 7, you can use a finally block to ensure that a resource is closed (the
+ * {@code Span} is ended and removed from the Context) regardless of whether the try statement
+ * completes normally or abruptly.
+ *
+ * <p>Example of usage prior to Java SE7:
+ *
+ * <pre>{@code
+ * class MyClass {
+ * private static Tracer tracer = Tracing.getTracer();
+ * void doWork {
+ * // Create a Span as a child of the current Span.
+ * Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan();
+ * try {
+ * tracer.getCurrentSpan().addAnnotation("my annotation");
+ * doSomeWork(); // Here the new span is in the current Context, so it can be used
+ * // implicitly anywhere down the stack. Anytime in this closure the span
+ * // can be accessed via tracer.getCurrentSpan().
+ * } finally {
+ * ss.close();
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>WARNING: The try-with-resources feature to auto-close spans as described above can sound
+ * very tempting due to its convenience, but it comes with an important and easy-to-miss
+ * trade-off: the span will be closed before any {@code catch} or {@code finally} blocks get a
+ * chance to execute. So if you need to catch any exceptions and log information about them (for
+ * example), then you do not want to use the try-with-resources shortcut because that logging will
+ * not be tagged with the span info of the span it logically falls under, and if you try to
+ * retrieve {@code Tracer.getCurrentSpan()} then you'll either get the parent span if one exists
+ * or {@code BlankSpan} if there was no parent span. This can be confusing and seem
+ * counter-intuitive, but it's the way try-with-resources works.
+ *
+ * @return an object that defines a scope where the newly created {@code Span} will be set to the
+ * current Context.
+ * @since 0.5
+ */
+ @MustBeClosed
+ public final Scope startScopedSpan() {
+ return CurrentSpanUtils.withSpan(startSpan(), /* endSpan= */ true);
+ }
+
+ /**
+ * Starts a new span and runs the given {@code Runnable} with the newly created {@code Span} as
+ * the current {@code Span}, and ends the {@code Span} after the {@code Runnable} is run.
+ *
+ * <p>Any error will end up as a {@link Status#UNKNOWN}.
+ *
+ * <pre><code>
+ * tracer.spanBuilder("MyRunnableSpan").startSpanAndRun(myRunnable);
+ * </code></pre>
+ *
+ * <p>It is equivalent with the following code:
+ *
+ * <pre><code>
+ * Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+ * Runnable newRunnable = tracer.withSpan(span, myRunnable);
+ * try {
+ * newRunnable.run();
+ * } finally {
+ * span.end();
+ * }
+ * </code></pre>
+ *
+ * @param runnable the {@code Runnable} to run in the {@code Span}.
+ * @since 0.11.0
+ */
+ public final void startSpanAndRun(final Runnable runnable) {
+ final Span span = startSpan();
+ CurrentSpanUtils.withSpan(span, /* endSpan= */ true, runnable).run();
+ }
+
+ /**
+ * Starts a new span and calls the given {@code Callable} with the newly created {@code Span} as
+ * the current {@code Span}, and ends the {@code Span} after the {@code Callable} is called.
+ *
+ * <p>Any error will end up as a {@link Status#UNKNOWN}.
+ *
+ * <pre><code>
+ * MyResult myResult = tracer.spanBuilder("MyCallableSpan").startSpanAndCall(myCallable);
+ * </code></pre>
+ *
+ * <p>It is equivalent with the following code:
+ *
+ * <pre><code>
+ * Span span = tracer.spanBuilder("MyCallableSpan").startSpan();
+ * {@code Callable<MyResult>} newCallable = tracer.withSpan(span, myCallable);
+ * MyResult myResult = null;
+ * try {
+ * myResult = newCallable.call();
+ * } finally {
+ * span.end();
+ * }
+ * );
+ * </code></pre>
+ *
+ * @param callable the {@code Callable} to run in the {@code Span}.
+ * @since 0.11.0
+ */
+ public final <V> V startSpanAndCall(Callable<V> callable) throws Exception {
+ final Span span = startSpan();
+ return CurrentSpanUtils.withSpan(span, /* endSpan= */ true, callable).call();
+ }
+
+ static final class NoopSpanBuilder extends SpanBuilder {
+ static NoopSpanBuilder createWithParent(String spanName, @Nullable Span parent) {
+ return new NoopSpanBuilder(spanName);
+ }
+
+ static NoopSpanBuilder createWithRemoteParent(
+ String spanName, @Nullable SpanContext remoteParentSpanContext) {
+ return new NoopSpanBuilder(spanName);
+ }
+
+ @Override
+ public Span startSpan() {
+ return BlankSpan.INSTANCE;
+ }
+
+ @Override
+ public SpanBuilder setSampler(@Nullable Sampler sampler) {
+ return this;
+ }
+
+ @Override
+ public SpanBuilder setParentLinks(List<Span> parentLinks) {
+ return this;
+ }
+
+ @Override
+ public SpanBuilder setRecordEvents(boolean recordEvents) {
+ return this;
+ }
+
+ @Override
+ public SpanBuilder setSpanKind(@Nullable Span.Kind spanKind) {
+ return this;
+ }
+
+ private NoopSpanBuilder(String name) {
+ Utils.checkNotNull(name, "name");
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/SpanContext.java b/api/src/main/java/io/opencensus/trace/SpanContext.java
new file mode 100644
index 00000000..49ed751b
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/SpanContext.java
@@ -0,0 +1,165 @@
+/*
+ * 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.trace;
+
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a span context. A span context contains the state that must propagate to
+ * child {@link Span}s and across process boundaries. It contains the identifiers (a {@link TraceId
+ * trace_id} and {@link SpanId span_id}) associated with the {@link Span} and a set of {@link
+ * TraceOptions options}.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class SpanContext {
+ private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+ private final TraceId traceId;
+ private final SpanId spanId;
+ private final TraceOptions traceOptions;
+ private final Tracestate tracestate;
+
+ /**
+ * The invalid {@code SpanContext}.
+ *
+ * @since 0.5
+ */
+ public static final SpanContext INVALID =
+ new SpanContext(TraceId.INVALID, SpanId.INVALID, TraceOptions.DEFAULT, TRACESTATE_DEFAULT);
+
+ /**
+ * Creates a new {@code SpanContext} with the given identifiers and options.
+ *
+ * @param traceId the trace identifier of the span context.
+ * @param spanId the span identifier of the span context.
+ * @param traceOptions the trace options for the span context.
+ * @return a new {@code SpanContext} with the given identifiers and options.
+ * @deprecated use {@link #create(TraceId, SpanId, TraceOptions, Tracestate)}.
+ */
+ @Deprecated
+ public static SpanContext create(TraceId traceId, SpanId spanId, TraceOptions traceOptions) {
+ return create(traceId, spanId, traceOptions, TRACESTATE_DEFAULT);
+ }
+
+ /**
+ * Creates a new {@code SpanContext} with the given identifiers and options.
+ *
+ * @param traceId the trace identifier of the span context.
+ * @param spanId the span identifier of the span context.
+ * @param traceOptions the trace options for the span context.
+ * @param tracestate the trace state for the span context.
+ * @return a new {@code SpanContext} with the given identifiers and options.
+ * @since 0.16
+ */
+ public static SpanContext create(
+ TraceId traceId, SpanId spanId, TraceOptions traceOptions, Tracestate tracestate) {
+ return new SpanContext(traceId, spanId, traceOptions, tracestate);
+ }
+
+ /**
+ * Returns the trace identifier associated with this {@code SpanContext}.
+ *
+ * @return the trace identifier associated with this {@code SpanContext}.
+ * @since 0.5
+ */
+ public TraceId getTraceId() {
+ return traceId;
+ }
+
+ /**
+ * Returns the span identifier associated with this {@code SpanContext}.
+ *
+ * @return the span identifier associated with this {@code SpanContext}.
+ * @since 0.5
+ */
+ public SpanId getSpanId() {
+ return spanId;
+ }
+
+ /**
+ * Returns the {@code TraceOptions} associated with this {@code SpanContext}.
+ *
+ * @return the {@code TraceOptions} associated with this {@code SpanContext}.
+ * @since 0.5
+ */
+ public TraceOptions getTraceOptions() {
+ return traceOptions;
+ }
+
+ /**
+ * Returns the {@code Tracestate} associated with this {@code SpanContext}.
+ *
+ * @return the {@code Tracestate} associated with this {@code SpanContext}.
+ * @since 0.5
+ */
+ public Tracestate getTracestate() {
+ return tracestate;
+ }
+
+ /**
+ * Returns true if this {@code SpanContext} is valid.
+ *
+ * @return true if this {@code SpanContext} is valid.
+ * @since 0.5
+ */
+ public boolean isValid() {
+ return traceId.isValid() && spanId.isValid();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (!(obj instanceof SpanContext)) {
+ return false;
+ }
+
+ SpanContext that = (SpanContext) obj;
+ return traceId.equals(that.traceId)
+ && spanId.equals(that.spanId)
+ && traceOptions.equals(that.traceOptions);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new Object[] {traceId, spanId, traceOptions});
+ }
+
+ @Override
+ public String toString() {
+ return "SpanContext{traceId="
+ + traceId
+ + ", spanId="
+ + spanId
+ + ", traceOptions="
+ + traceOptions
+ + "}";
+ }
+
+ private SpanContext(
+ TraceId traceId, SpanId spanId, TraceOptions traceOptions, Tracestate tracestate) {
+ this.traceId = traceId;
+ this.spanId = spanId;
+ this.traceOptions = traceOptions;
+ this.tracestate = tracestate;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/SpanId.java b/api/src/main/java/io/opencensus/trace/SpanId.java
new file mode 100644
index 00000000..c43fa6b0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/SpanId.java
@@ -0,0 +1,214 @@
+/*
+ * 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.Arrays;
+import java.util.Random;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a span identifier. A valid span identifier is an 8-byte array with at
+ * least one non-zero byte.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class SpanId implements Comparable<SpanId> {
+ /**
+ * The size in bytes of the {@code SpanId}.
+ *
+ * @since 0.5
+ */
+ public static final int SIZE = 8;
+
+ private static final int HEX_SIZE = 2 * SIZE;
+
+ /**
+ * The invalid {@code SpanId}. All bytes are 0.
+ *
+ * @since 0.5
+ */
+ public static final SpanId INVALID = new SpanId(new byte[SIZE]);
+
+ // The internal representation of the SpanId.
+ private final byte[] bytes;
+
+ private SpanId(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ /**
+ * Returns a {@code SpanId} built from a byte representation.
+ *
+ * <p>Equivalent with:
+ *
+ * <pre>{@code
+ * SpanId.fromBytes(buffer, 0);
+ * }</pre>
+ *
+ * @param buffer the representation of the {@code SpanId}.
+ * @return a {@code SpanId} whose representation is given by the {@code buffer} parameter.
+ * @throws NullPointerException if {@code buffer} is null.
+ * @throws IllegalArgumentException if {@code buffer.length} is not {@link SpanId#SIZE}.
+ * @since 0.5
+ */
+ public static SpanId fromBytes(byte[] buffer) {
+ Utils.checkNotNull(buffer, "buffer");
+ Utils.checkArgument(
+ buffer.length == SIZE, "Invalid size: expected %s, got %s", SIZE, buffer.length);
+ byte[] bytesCopied = Arrays.copyOf(buffer, SIZE);
+ return new SpanId(bytesCopied);
+ }
+
+ /**
+ * Returns a {@code SpanId} whose representation is copied from the {@code src} beginning at the
+ * {@code srcOffset} offset.
+ *
+ * @param src the buffer where the representation of the {@code SpanId} is copied.
+ * @param srcOffset the offset in the buffer where the representation of the {@code SpanId}
+ * begins.
+ * @return a {@code SpanId} whose representation is copied from the buffer.
+ * @throws NullPointerException if {@code src} is null.
+ * @throws IndexOutOfBoundsException if {@code srcOffset+SpanId.SIZE} is greater than {@code
+ * src.length}.
+ * @since 0.5
+ */
+ public static SpanId fromBytes(byte[] src, int srcOffset) {
+ byte[] bytes = new byte[SIZE];
+ System.arraycopy(src, srcOffset, bytes, 0, SIZE);
+ return new SpanId(bytes);
+ }
+
+ /**
+ * Returns a {@code SpanId} built from a lowercase base16 representation.
+ *
+ * @param src the lowercase base16 representation.
+ * @return a {@code SpanId} built from a lowercase base16 representation.
+ * @throws NullPointerException if {@code src} is null.
+ * @throws IllegalArgumentException if {@code src.length} is not {@code 2 * SpanId.SIZE} OR if the
+ * {@code str} has invalid characters.
+ * @since 0.11
+ */
+ public static SpanId fromLowerBase16(CharSequence src) {
+ Utils.checkArgument(
+ src.length() == HEX_SIZE, "Invalid size: expected %s, got %s", HEX_SIZE, src.length());
+ return new SpanId(LowerCaseBase16Encoding.decodeToBytes(src));
+ }
+
+ /**
+ * Generates a new random {@code SpanId}.
+ *
+ * @param random The random number generator.
+ * @return a valid new {@code SpanId}.
+ * @since 0.5
+ */
+ public static SpanId generateRandomId(Random random) {
+ byte[] bytes = new byte[SIZE];
+ do {
+ random.nextBytes(bytes);
+ } while (Arrays.equals(bytes, INVALID.bytes));
+ return new SpanId(bytes);
+ }
+
+ /**
+ * Returns the byte representation of the {@code SpanId}.
+ *
+ * @return the byte representation of the {@code SpanId}.
+ * @since 0.5
+ */
+ public byte[] getBytes() {
+ return Arrays.copyOf(bytes, SIZE);
+ }
+
+ /**
+ * Copies the byte array representations of the {@code SpanId} into the {@code dest} beginning at
+ * the {@code destOffset} offset.
+ *
+ * <p>Equivalent with (but faster because it avoids any new allocations):
+ *
+ * <pre>{@code
+ * System.arraycopy(getBytes(), 0, dest, destOffset, SpanId.SIZE);
+ * }</pre>
+ *
+ * @param dest the destination buffer.
+ * @param destOffset the starting offset in the destination buffer.
+ * @throws NullPointerException if {@code dest} is null.
+ * @throws IndexOutOfBoundsException if {@code destOffset+SpanId.SIZE} is greater than {@code
+ * dest.length}.
+ * @since 0.5
+ */
+ public void copyBytesTo(byte[] dest, int destOffset) {
+ System.arraycopy(bytes, 0, dest, destOffset, SIZE);
+ }
+
+ /**
+ * Returns whether the span identifier is valid. A valid span identifier is an 8-byte array with
+ * at least one non-zero byte.
+ *
+ * @return {@code true} if the span identifier is valid.
+ * @since 0.5
+ */
+ public boolean isValid() {
+ return !Arrays.equals(bytes, INVALID.bytes);
+ }
+
+ /**
+ * Returns the lowercase base16 encoding of this {@code SpanId}.
+ *
+ * @return the lowercase base16 encoding of this {@code SpanId}.
+ * @since 0.11
+ */
+ public String toLowerBase16() {
+ return LowerCaseBase16Encoding.encodeToString(bytes);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (!(obj instanceof SpanId)) {
+ return false;
+ }
+
+ SpanId that = (SpanId) obj;
+ return Arrays.equals(bytes, that.bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(bytes);
+ }
+
+ @Override
+ public String toString() {
+ return "SpanId{spanId=" + toLowerBase16() + "}";
+ }
+
+ @Override
+ public int compareTo(SpanId that) {
+ for (int i = 0; i < SIZE; i++) {
+ if (bytes[i] != that.bytes[i]) {
+ return bytes[i] < that.bytes[i] ? -1 : 1;
+ }
+ }
+ return 0;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/Status.java b/api/src/main/java/io/opencensus/trace/Status.java
new file mode 100644
index 00000000..1fa85085
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Status.java
@@ -0,0 +1,469 @@
+/*
+ * 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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.TreeMap;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/*>>>
+import org.checkerframework.dataflow.qual.Deterministic;
+*/
+
+/**
+ * Defines the status of a {@link Span} by providing a standard {@link CanonicalCode} in conjunction
+ * with an optional descriptive message. Instances of {@code Status} are created by starting with
+ * the template for the appropriate {@link Status.CanonicalCode} and supplementing it with
+ * additional information: {@code Status.NOT_FOUND.withDescription("Could not find
+ * 'important_file.txt'");}
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class Status {
+ /**
+ * The set of canonical status codes. If new codes are added over time they must choose a
+ * numerical value that does not collide with any previously used value.
+ *
+ * @since 0.5
+ */
+ public enum CanonicalCode {
+ /**
+ * The operation completed successfully.
+ *
+ * @since 0.5
+ */
+ OK(0),
+
+ /**
+ * The operation was cancelled (typically by the caller).
+ *
+ * @since 0.5
+ */
+ CANCELLED(1),
+
+ /**
+ * Unknown error. An example of where this error may be returned is if a Status value received
+ * from another address space belongs to an error-space that is not known in this address space.
+ * Also errors raised by APIs that do not return enough error information may be converted to
+ * this error.
+ *
+ * @since 0.5
+ */
+ UNKNOWN(2),
+
+ /**
+ * Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION.
+ * INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the
+ * system (e.g., a malformed file name).
+ *
+ * @since 0.5
+ */
+ INVALID_ARGUMENT(3),
+
+ /**
+ * Deadline expired before operation could complete. For operations that change the state of the
+ * system, this error may be returned even if the operation has completed successfully. For
+ * example, a successful response from a server could have been delayed long enough for the
+ * deadline to expire.
+ *
+ * @since 0.5
+ */
+ DEADLINE_EXCEEDED(4),
+
+ /**
+ * Some requested entity (e.g., file or directory) was not found.
+ *
+ * @since 0.5
+ */
+ NOT_FOUND(5),
+
+ /**
+ * Some entity that we attempted to create (e.g., file or directory) already exists.
+ *
+ * @since 0.5
+ */
+ ALREADY_EXISTS(6),
+
+ /**
+ * The caller does not have permission to execute the specified operation. PERMISSION_DENIED
+ * must not be used for rejections caused by exhausting some resource (use RESOURCE_EXHAUSTED
+ * instead for those errors). PERMISSION_DENIED must not be used if the caller cannot be
+ * identified (use UNAUTHENTICATED instead for those errors).
+ *
+ * @since 0.5
+ */
+ PERMISSION_DENIED(7),
+
+ /**
+ * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
+ * is out of space.
+ *
+ * @since 0.5
+ */
+ RESOURCE_EXHAUSTED(8),
+
+ /**
+ * Operation was rejected because the system is not in a state required for the operation's
+ * execution. For example, directory to be deleted may be non-empty, an rmdir operation is
+ * applied to a non-directory, etc.
+ *
+ * <p>A litmus test that may help a service implementor in deciding between FAILED_PRECONDITION,
+ * ABORTED, and UNAVAILABLE: (a) Use UNAVAILABLE if the client can retry just the failing call.
+ * (b) Use ABORTED if the client should retry at a higher-level (e.g., restarting a
+ * read-modify-write sequence). (c) Use FAILED_PRECONDITION if the client should not retry until
+ * the system state has been explicitly fixed. E.g., if an "rmdir" fails because the directory
+ * is non-empty, FAILED_PRECONDITION should be returned since the client should not retry unless
+ * they have first fixed up the directory by deleting files from it.
+ *
+ * @since 0.5
+ */
+ FAILED_PRECONDITION(9),
+
+ /**
+ * The operation was aborted, typically due to a concurrency issue like sequencer check
+ * failures, transaction aborts, etc.
+ *
+ * <p>See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE.
+ *
+ * @since 0.5
+ */
+ ABORTED(10),
+
+ /**
+ * Operation was attempted past the valid range. E.g., seeking or reading past end of file.
+ *
+ * <p>Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed if the system
+ * state changes. For example, a 32-bit file system will generate INVALID_ARGUMENT if asked to
+ * read at an offset that is not in the range [0,2^32-1], but it will generate OUT_OF_RANGE if
+ * asked to read from an offset past the current file size.
+ *
+ * <p>There is a fair bit of overlap between FAILED_PRECONDITION and OUT_OF_RANGE. We recommend
+ * using OUT_OF_RANGE (the more specific error) when it applies so that callers who are
+ * iterating through a space can easily look for an OUT_OF_RANGE error to detect when they are
+ * done.
+ *
+ * @since 0.5
+ */
+ OUT_OF_RANGE(11),
+
+ /**
+ * Operation is not implemented or not supported/enabled in this service.
+ *
+ * @since 0.5
+ */
+ UNIMPLEMENTED(12),
+
+ /**
+ * Internal errors. Means some invariants expected by underlying system has been broken. If you
+ * see one of these errors, something is very broken.
+ *
+ * @since 0.5
+ */
+ INTERNAL(13),
+
+ /**
+ * The service is currently unavailable. This is a most likely a transient condition and may be
+ * corrected by retrying with a backoff.
+ *
+ * <p>See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE.
+ *
+ * @since 0.5
+ */
+ UNAVAILABLE(14),
+
+ /**
+ * Unrecoverable data loss or corruption.
+ *
+ * @since 0.5
+ */
+ DATA_LOSS(15),
+
+ /**
+ * The request does not have valid authentication credentials for the operation.
+ *
+ * @since 0.5
+ */
+ UNAUTHENTICATED(16);
+
+ private final int value;
+
+ private CanonicalCode(int value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the numerical value of the code.
+ *
+ * @return the numerical value of the code.
+ * @since 0.5
+ */
+ public int value() {
+ return value;
+ }
+
+ /**
+ * Returns the status that has the current {@code CanonicalCode}..
+ *
+ * @return the status that has the current {@code CanonicalCode}.
+ * @since 0.5
+ */
+ public Status toStatus() {
+ return STATUS_LIST.get(value);
+ }
+ }
+
+ // Create the canonical list of Status instances indexed by their code values.
+ private static final List<Status> STATUS_LIST = buildStatusList();
+
+ private static List<Status> buildStatusList() {
+ TreeMap<Integer, Status> canonicalizer = new TreeMap<Integer, Status>();
+ for (CanonicalCode code : CanonicalCode.values()) {
+ Status replaced = canonicalizer.put(code.value(), new Status(code, null));
+ if (replaced != null) {
+ throw new IllegalStateException(
+ "Code value duplication between "
+ + replaced.getCanonicalCode().name()
+ + " & "
+ + code.name());
+ }
+ }
+ return Collections.unmodifiableList(new ArrayList<Status>(canonicalizer.values()));
+ }
+
+ // A pseudo-enum of Status instances mapped 1:1 with values in CanonicalCode. This simplifies
+ // construction patterns for derived instances of Status.
+ /**
+ * The operation completed successfully.
+ *
+ * @since 0.5
+ */
+ public static final Status OK = CanonicalCode.OK.toStatus();
+
+ /**
+ * The operation was cancelled (typically by the caller).
+ *
+ * @since 0.5
+ */
+ public static final Status CANCELLED = CanonicalCode.CANCELLED.toStatus();
+
+ /**
+ * Unknown error. See {@link CanonicalCode#UNKNOWN}.
+ *
+ * @since 0.5
+ */
+ public static final Status UNKNOWN = CanonicalCode.UNKNOWN.toStatus();
+
+ /**
+ * Client specified an invalid argument. See {@link CanonicalCode#INVALID_ARGUMENT}.
+ *
+ * @since 0.5
+ */
+ public static final Status INVALID_ARGUMENT = CanonicalCode.INVALID_ARGUMENT.toStatus();
+
+ /**
+ * Deadline expired before operation could complete. See {@link CanonicalCode#DEADLINE_EXCEEDED}.
+ *
+ * @since 0.5
+ */
+ public static final Status DEADLINE_EXCEEDED = CanonicalCode.DEADLINE_EXCEEDED.toStatus();
+
+ /**
+ * Some requested entity (e.g., file or directory) was not found.
+ *
+ * @since 0.5
+ */
+ public static final Status NOT_FOUND = CanonicalCode.NOT_FOUND.toStatus();
+
+ /**
+ * Some entity that we attempted to create (e.g., file or directory) already exists.
+ *
+ * @since 0.5
+ */
+ public static final Status ALREADY_EXISTS = CanonicalCode.ALREADY_EXISTS.toStatus();
+
+ /**
+ * The caller does not have permission to execute the specified operation. See {@link
+ * CanonicalCode#PERMISSION_DENIED}.
+ *
+ * @since 0.5
+ */
+ public static final Status PERMISSION_DENIED = CanonicalCode.PERMISSION_DENIED.toStatus();
+
+ /**
+ * The request does not have valid authentication credentials for the operation.
+ *
+ * @since 0.5
+ */
+ public static final Status UNAUTHENTICATED = CanonicalCode.UNAUTHENTICATED.toStatus();
+
+ /**
+ * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system
+ * is out of space.
+ *
+ * @since 0.5
+ */
+ public static final Status RESOURCE_EXHAUSTED = CanonicalCode.RESOURCE_EXHAUSTED.toStatus();
+
+ /**
+ * Operation was rejected because the system is not in a state required for the operation's
+ * execution. See {@link CanonicalCode#FAILED_PRECONDITION}.
+ *
+ * @since 0.5
+ */
+ public static final Status FAILED_PRECONDITION = CanonicalCode.FAILED_PRECONDITION.toStatus();
+
+ /**
+ * The operation was aborted, typically due to a concurrency issue like sequencer check failures,
+ * transaction aborts, etc. See {@link CanonicalCode#ABORTED}.
+ *
+ * @since 0.5
+ */
+ public static final Status ABORTED = CanonicalCode.ABORTED.toStatus();
+
+ /**
+ * Operation was attempted past the valid range. See {@link CanonicalCode#OUT_OF_RANGE}.
+ *
+ * @since 0.5
+ */
+ public static final Status OUT_OF_RANGE = CanonicalCode.OUT_OF_RANGE.toStatus();
+
+ /**
+ * Operation is not implemented or not supported/enabled in this service.
+ *
+ * @since 0.5
+ */
+ public static final Status UNIMPLEMENTED = CanonicalCode.UNIMPLEMENTED.toStatus();
+
+ /**
+ * Internal errors. See {@link CanonicalCode#INTERNAL}.
+ *
+ * @since 0.5
+ */
+ public static final Status INTERNAL = CanonicalCode.INTERNAL.toStatus();
+
+ /**
+ * The service is currently unavailable. See {@link CanonicalCode#UNAVAILABLE}.
+ *
+ * @since 0.5
+ */
+ public static final Status UNAVAILABLE = CanonicalCode.UNAVAILABLE.toStatus();
+
+ /**
+ * Unrecoverable data loss or corruption.
+ *
+ * @since 0.5
+ */
+ public static final Status DATA_LOSS = CanonicalCode.DATA_LOSS.toStatus();
+
+ // The canonical code of this message.
+ private final CanonicalCode canonicalCode;
+
+ // An additional error message.
+ @Nullable private final String description;
+
+ private Status(CanonicalCode canonicalCode, @Nullable String description) {
+ this.canonicalCode = Utils.checkNotNull(canonicalCode, "canonicalCode");
+ this.description = description;
+ }
+
+ /**
+ * Creates a derived instance of {@code Status} with the given description.
+ *
+ * @param description the new description of the {@code Status}.
+ * @return The newly created {@code Status} with the given description.
+ * @since 0.5
+ */
+ public Status withDescription(String description) {
+ if (Utils.equalsObjects(this.description, description)) {
+ return this;
+ }
+ return new Status(this.canonicalCode, description);
+ }
+
+ /**
+ * Returns the canonical status code.
+ *
+ * @return the canonical status code.
+ * @since 0.5
+ */
+ public CanonicalCode getCanonicalCode() {
+ return canonicalCode;
+ }
+
+ /**
+ * Returns the description of this {@code Status} for human consumption.
+ *
+ * @return the description of this {@code Status}.
+ * @since 0.5
+ */
+ @Nullable
+ /*@Deterministic*/
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Returns {@code true} if this {@code Status} is OK, i.e., not an error.
+ *
+ * @return {@code true} if this {@code Status} is OK.
+ * @since 0.5
+ */
+ public boolean isOk() {
+ return CanonicalCode.OK == canonicalCode;
+ }
+
+ /**
+ * Equality on Statuses is not well defined. Instead, do comparison based on their CanonicalCode
+ * with {@link #getCanonicalCode}. The description of the Status is unlikely to be stable, and
+ * additional fields may be added to Status in the future.
+ */
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (!(obj instanceof Status)) {
+ return false;
+ }
+
+ Status that = (Status) obj;
+ return canonicalCode == that.canonicalCode
+ && Utils.equalsObjects(description, that.description);
+ }
+
+ /**
+ * Hash codes on Statuses are not well defined.
+ *
+ * @see #equals
+ */
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new Object[] {canonicalCode, description});
+ }
+
+ @Override
+ public String toString() {
+ return "Status{canonicalCode=" + canonicalCode + ", description=" + description + "}";
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/TraceComponent.java b/api/src/main/java/io/opencensus/trace/TraceComponent.java
new file mode 100644
index 00000000..d98d0f9e
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/TraceComponent.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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.internal.ZeroTimeClock;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/**
+ * Class that holds the implementation instances for {@link Tracer}, {@link PropagationComponent},
+ * {@link Clock}, {@link ExportComponent} and {@link TraceConfig}.
+ *
+ * <p>Unless otherwise noted all methods (on component) results are cacheable.
+ *
+ * @since 0.5
+ */
+public abstract class TraceComponent {
+
+ /**
+ * Returns the {@link Tracer} with the provided implementations. If no implementation is provided
+ * then no-op implementations will be used.
+ *
+ * @return the {@code Tracer} implementation.
+ * @since 0.5
+ */
+ public abstract Tracer getTracer();
+
+ /**
+ * Returns the {@link PropagationComponent} with the provided implementation. If no implementation
+ * is provided then no-op implementation will be used.
+ *
+ * @return the {@code PropagationComponent} implementation.
+ * @since 0.5
+ */
+ public abstract PropagationComponent getPropagationComponent();
+
+ /**
+ * Returns the {@link Clock} with the provided implementation.
+ *
+ * @return the {@code Clock} implementation.
+ * @since 0.5
+ */
+ public abstract Clock getClock();
+
+ /**
+ * Returns the {@link ExportComponent} with the provided implementation. If no implementation is
+ * provided then no-op implementations will be used.
+ *
+ * @return the {@link ExportComponent} implementation.
+ * @since 0.5
+ */
+ public abstract ExportComponent getExportComponent();
+
+ /**
+ * Returns the {@link TraceConfig} with the provided implementation. If no implementation is
+ * provided then no-op implementations will be used.
+ *
+ * @return the {@link TraceConfig} implementation.
+ * @since 0.5
+ */
+ public abstract TraceConfig getTraceConfig();
+
+ /**
+ * Returns an instance that contains no-op implementations for all the instances.
+ *
+ * @return an instance that contains no-op implementations for all the instances.
+ */
+ static TraceComponent newNoopTraceComponent() {
+ return new NoopTraceComponent();
+ }
+
+ private static final class NoopTraceComponent extends TraceComponent {
+ private final ExportComponent noopExportComponent = ExportComponent.newNoopExportComponent();
+
+ @Override
+ public Tracer getTracer() {
+ return Tracer.getNoopTracer();
+ }
+
+ @Override
+ public PropagationComponent getPropagationComponent() {
+ return PropagationComponent.getNoopPropagationComponent();
+ }
+
+ @Override
+ public Clock getClock() {
+ return ZeroTimeClock.getInstance();
+ }
+
+ @Override
+ public ExportComponent getExportComponent() {
+ return noopExportComponent;
+ }
+
+ @Override
+ public TraceConfig getTraceConfig() {
+ return TraceConfig.getNoopTraceConfig();
+ }
+
+ private NoopTraceComponent() {}
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/TraceId.java b/api/src/main/java/io/opencensus/trace/TraceId.java
new file mode 100644
index 00000000..465e4d4a
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/TraceId.java
@@ -0,0 +1,236 @@
+/*
+ * 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.trace;
+
+import io.opencensus.common.Internal;
+import io.opencensus.internal.Utils;
+import java.util.Arrays;
+import java.util.Random;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents a trace identifier. A valid trace identifier is a 16-byte array with at
+ * least one non-zero byte.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class TraceId implements Comparable<TraceId> {
+ /**
+ * The size in bytes of the {@code TraceId}.
+ *
+ * @since 0.5
+ */
+ public static final int SIZE = 16;
+
+ private static final int HEX_SIZE = 32;
+
+ /**
+ * The invalid {@code TraceId}. All bytes are '\0'.
+ *
+ * @since 0.5
+ */
+ public static final TraceId INVALID = new TraceId(new byte[SIZE]);
+
+ // The internal representation of the TraceId.
+ private final byte[] bytes;
+
+ private TraceId(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ /**
+ * Returns a {@code TraceId} built from a byte representation.
+ *
+ * <p>Equivalent with:
+ *
+ * <pre>{@code
+ * TraceId.fromBytes(buffer, 0);
+ * }</pre>
+ *
+ * @param buffer the representation of the {@code TraceId}.
+ * @return a {@code TraceId} whose representation is given by the {@code buffer} parameter.
+ * @throws NullPointerException if {@code buffer} is null.
+ * @throws IllegalArgumentException if {@code buffer.length} is not {@link TraceId#SIZE}.
+ * @since 0.5
+ */
+ public static TraceId fromBytes(byte[] buffer) {
+ Utils.checkNotNull(buffer, "buffer");
+ Utils.checkArgument(
+ buffer.length == SIZE, "Invalid size: expected %s, got %s", SIZE, buffer.length);
+ byte[] bytesCopied = Arrays.copyOf(buffer, SIZE);
+ return new TraceId(bytesCopied);
+ }
+
+ /**
+ * Returns a {@code TraceId} whose representation is copied from the {@code src} beginning at the
+ * {@code srcOffset} offset.
+ *
+ * @param src the buffer where the representation of the {@code TraceId} is copied.
+ * @param srcOffset the offset in the buffer where the representation of the {@code TraceId}
+ * begins.
+ * @return a {@code TraceId} whose representation is copied from the buffer.
+ * @throws NullPointerException if {@code src} is null.
+ * @throws IndexOutOfBoundsException if {@code srcOffset+TraceId.SIZE} is greater than {@code
+ * src.length}.
+ * @since 0.5
+ */
+ public static TraceId fromBytes(byte[] src, int srcOffset) {
+ byte[] bytes = new byte[SIZE];
+ System.arraycopy(src, srcOffset, bytes, 0, SIZE);
+ return new TraceId(bytes);
+ }
+
+ /**
+ * Returns a {@code TraceId} built from a lowercase base16 representation.
+ *
+ * @param src the lowercase base16 representation.
+ * @return a {@code TraceId} built from a lowercase base16 representation.
+ * @throws NullPointerException if {@code src} is null.
+ * @throws IllegalArgumentException if {@code src.length} is not {@code 2 * TraceId.SIZE} OR if
+ * the {@code str} has invalid characters.
+ * @since 0.11
+ */
+ public static TraceId fromLowerBase16(CharSequence src) {
+ Utils.checkArgument(
+ src.length() == HEX_SIZE, "Invalid size: expected %s, got %s", HEX_SIZE, src.length());
+ return new TraceId(LowerCaseBase16Encoding.decodeToBytes(src));
+ }
+
+ /**
+ * Generates a new random {@code TraceId}.
+ *
+ * @param random the random number generator.
+ * @return a new valid {@code TraceId}.
+ * @since 0.5
+ */
+ public static TraceId generateRandomId(Random random) {
+ byte[] bytes = new byte[SIZE];
+ do {
+ random.nextBytes(bytes);
+ } while (Arrays.equals(bytes, INVALID.bytes));
+ return new TraceId(bytes);
+ }
+
+ /**
+ * Returns the 16-bytes array representation of the {@code TraceId}.
+ *
+ * @return the 16-bytes array representation of the {@code TraceId}.
+ * @since 0.5
+ */
+ public byte[] getBytes() {
+ return Arrays.copyOf(bytes, SIZE);
+ }
+
+ /**
+ * Copies the byte array representations of the {@code TraceId} into the {@code dest} beginning at
+ * the {@code destOffset} offset.
+ *
+ * <p>Equivalent with (but faster because it avoids any new allocations):
+ *
+ * <pre>{@code
+ * System.arraycopy(getBytes(), 0, dest, destOffset, TraceId.SIZE);
+ * }</pre>
+ *
+ * @param dest the destination buffer.
+ * @param destOffset the starting offset in the destination buffer.
+ * @throws NullPointerException if {@code dest} is null.
+ * @throws IndexOutOfBoundsException if {@code destOffset+TraceId.SIZE} is greater than {@code
+ * dest.length}.
+ * @since 0.5
+ */
+ public void copyBytesTo(byte[] dest, int destOffset) {
+ System.arraycopy(bytes, 0, dest, destOffset, SIZE);
+ }
+
+ /**
+ * Returns whether the {@code TraceId} is valid. A valid trace identifier is a 16-byte array with
+ * at least one non-zero byte.
+ *
+ * @return {@code true} if the {@code TraceId} is valid.
+ * @since 0.5
+ */
+ public boolean isValid() {
+ return !Arrays.equals(bytes, INVALID.bytes);
+ }
+
+ /**
+ * Returns the lowercase base16 encoding of this {@code TraceId}.
+ *
+ * @return the lowercase base16 encoding of this {@code TraceId}.
+ * @since 0.11
+ */
+ public String toLowerBase16() {
+ return LowerCaseBase16Encoding.encodeToString(bytes);
+ }
+
+ /**
+ * Returns the lower 8 bytes of the trace-id as a long value, assuming little-endian order. This
+ * is used in ProbabilitySampler.
+ *
+ * <p>This method is marked as internal and subject to change.
+ *
+ * @return the lower 8 bytes of the trace-id as a long value, assuming little-endian order.
+ */
+ @Internal
+ public long getLowerLong() {
+ long result = 0;
+ for (int i = 0; i < Long.SIZE / Byte.SIZE; i++) {
+ result <<= Byte.SIZE;
+ result |= (bytes[i] & 0xff);
+ }
+ if (result < 0) {
+ return -result;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (!(obj instanceof TraceId)) {
+ return false;
+ }
+
+ TraceId that = (TraceId) obj;
+ return Arrays.equals(bytes, that.bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(bytes);
+ }
+
+ @Override
+ public String toString() {
+ return "TraceId{traceId=" + toLowerBase16() + "}";
+ }
+
+ @Override
+ public int compareTo(TraceId that) {
+ for (int i = 0; i < SIZE; i++) {
+ if (bytes[i] != that.bytes[i]) {
+ return bytes[i] < that.bytes[i] ? -1 : 1;
+ }
+ }
+ return 0;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/TraceOptions.java b/api/src/main/java/io/opencensus/trace/TraceOptions.java
new file mode 100644
index 00000000..218f4dab
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/TraceOptions.java
@@ -0,0 +1,280 @@
+/*
+ * 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.trace;
+
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Utils;
+import java.util.Arrays;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A class that represents global trace options. These options are propagated to all child {@link
+ * io.opencensus.trace.Span spans}. These determine features such as whether a {@code Span} should
+ * be traced. It is implemented as a bitmask.
+ *
+ * @since 0.5
+ */
+@Immutable
+public final class TraceOptions {
+ // Default options. Nothing set.
+ private static final byte DEFAULT_OPTIONS = 0;
+ // Bit to represent whether trace is sampled or not.
+ private static final byte IS_SAMPLED = 0x1;
+
+ /**
+ * The size in bytes of the {@code TraceOptions}.
+ *
+ * @since 0.5
+ */
+ public static final int SIZE = 1;
+
+ /**
+ * The default {@code TraceOptions}.
+ *
+ * @since 0.5
+ */
+ public static final TraceOptions DEFAULT = fromByte(DEFAULT_OPTIONS);
+
+ // The set of enabled features is determined by all the enabled bits.
+ private final byte options;
+
+ // Creates a new {@code TraceOptions} with the given options.
+ private TraceOptions(byte options) {
+ this.options = options;
+ }
+
+ /**
+ * Returns a {@code TraceOptions} built from a byte representation.
+ *
+ * <p>Equivalent with:
+ *
+ * <pre>{@code
+ * TraceOptions.fromBytes(buffer, 0);
+ * }</pre>
+ *
+ * @param buffer the representation of the {@code TraceOptions}.
+ * @return a {@code TraceOptions} whose representation is given by the {@code buffer} parameter.
+ * @throws NullPointerException if {@code buffer} is null.
+ * @throws IllegalArgumentException if {@code buffer.length} is not {@link TraceOptions#SIZE}.
+ * @since 0.5
+ * @deprecated use {@link #fromByte(byte)}.
+ */
+ @Deprecated
+ public static TraceOptions fromBytes(byte[] buffer) {
+ Utils.checkNotNull(buffer, "buffer");
+ Utils.checkArgument(
+ buffer.length == SIZE, "Invalid size: expected %s, got %s", SIZE, buffer.length);
+ return fromByte(buffer[0]);
+ }
+
+ /**
+ * Returns a {@code TraceOptions} whose representation is copied from the {@code src} beginning at
+ * the {@code srcOffset} offset.
+ *
+ * @param src the buffer where the representation of the {@code TraceOptions} is copied.
+ * @param srcOffset the offset in the buffer where the representation of the {@code TraceOptions}
+ * begins.
+ * @return a {@code TraceOptions} whose representation is copied from the buffer.
+ * @throws NullPointerException if {@code src} is null.
+ * @throws IndexOutOfBoundsException if {@code srcOffset+TraceOptions.SIZE} is greater than {@code
+ * src.length}.
+ * @since 0.5
+ * @deprecated use {@link #fromByte(byte)}.
+ */
+ @Deprecated
+ public static TraceOptions fromBytes(byte[] src, int srcOffset) {
+ Utils.checkIndex(srcOffset, src.length);
+ return fromByte(src[srcOffset]);
+ }
+
+ /**
+ * Returns a {@code TraceOptions} whose representation is {@code src}.
+ *
+ * @param src the byte representation of the {@code TraceOptions}.
+ * @return a {@code TraceOptions} whose representation is {@code src}.
+ * @since 0.16
+ */
+ public static TraceOptions fromByte(byte src) {
+ // TODO(bdrutu): OPTIMIZATION: Cache all the 256 possible objects and return from the cache.
+ return new TraceOptions(src);
+ }
+
+ /**
+ * Returns the one byte representation of the {@code TraceOptions}.
+ *
+ * @return the one byte representation of the {@code TraceOptions}.
+ * @since 0.16
+ */
+ public byte getByte() {
+ return options;
+ }
+
+ /**
+ * Returns the 1-byte array representation of the {@code TraceOptions}.
+ *
+ * @return the 1-byte array representation of the {@code TraceOptions}.
+ * @since 0.5
+ * @deprecated use {@link #getByte()}.
+ */
+ @Deprecated
+ public byte[] getBytes() {
+ byte[] bytes = new byte[SIZE];
+ bytes[0] = options;
+ return bytes;
+ }
+
+ /**
+ * Copies the byte representations of the {@code TraceOptions} into the {@code dest} beginning at
+ * the {@code destOffset} offset.
+ *
+ * <p>Equivalent with (but faster because it avoids any new allocations):
+ *
+ * <pre>{@code
+ * System.arraycopy(getBytes(), 0, dest, destOffset, TraceOptions.SIZE);
+ * }</pre>
+ *
+ * @param dest the destination buffer.
+ * @param destOffset the starting offset in the destination buffer.
+ * @throws NullPointerException if {@code dest} is null.
+ * @throws IndexOutOfBoundsException if {@code destOffset+TraceOptions.SIZE} is greater than
+ * {@code dest.length}.
+ * @since 0.5
+ */
+ public void copyBytesTo(byte[] dest, int destOffset) {
+ Utils.checkIndex(destOffset, dest.length);
+ dest[destOffset] = options;
+ }
+
+ /**
+ * Returns a new {@link Builder} with default options.
+ *
+ * @return a new {@code Builder} with default options.
+ * @since 0.5
+ */
+ public static Builder builder() {
+ return new Builder(DEFAULT_OPTIONS);
+ }
+
+ /**
+ * Returns a new {@link Builder} with all given options set.
+ *
+ * @param traceOptions the given options set.
+ * @return a new {@code Builder} with all given options set.
+ * @since 0.5
+ */
+ public static Builder builder(TraceOptions traceOptions) {
+ return new Builder(traceOptions.options);
+ }
+
+ /**
+ * Returns a boolean indicating whether this {@code Span} is part of a sampled trace and data
+ * should be exported to a persistent store.
+ *
+ * @return a boolean indicating whether the trace is sampled.
+ * @since 0.5
+ */
+ public boolean isSampled() {
+ return hasOption(IS_SAMPLED);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (!(obj instanceof TraceOptions)) {
+ return false;
+ }
+
+ TraceOptions that = (TraceOptions) obj;
+ return options == that.options;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new byte[] {options});
+ }
+
+ @Override
+ public String toString() {
+ return "TraceOptions{sampled=" + isSampled() + "}";
+ }
+
+ /**
+ * Builder class for {@link TraceOptions}.
+ *
+ * @since 0.5
+ */
+ public static final class Builder {
+ private byte options;
+
+ private Builder(byte options) {
+ this.options = options;
+ }
+
+ /**
+ * Sets the sampling bit in the options to true.
+ *
+ * @deprecated Use {@code Builder.setIsSampled(true)}.
+ * @return this.
+ * @since 0.5
+ */
+ @Deprecated
+ public Builder setIsSampled() {
+ return setIsSampled(true);
+ }
+
+ /**
+ * Sets the sampling bit in the options.
+ *
+ * @param isSampled the sampling bit.
+ * @return this.
+ * @since 0.7
+ */
+ public Builder setIsSampled(boolean isSampled) {
+ if (isSampled) {
+ options = (byte) (options | IS_SAMPLED);
+ } else {
+ options = (byte) (options & ~IS_SAMPLED);
+ ;
+ }
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@code TraceOptions} with the desired options.
+ *
+ * @return a {@code TraceOptions} with the desired options.
+ * @since 0.5
+ */
+ public TraceOptions build() {
+ return fromByte(options);
+ }
+ }
+
+ // Returns the current set of options bitmask.
+ @DefaultVisibilityForTesting
+ byte getOptions() {
+ return options;
+ }
+
+ private boolean hasOption(int mask) {
+ return (this.options & mask) != 0;
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/Tracer.java b/api/src/main/java/io/opencensus/trace/Tracer.java
new file mode 100644
index 00000000..a2c0a239
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Tracer.java
@@ -0,0 +1,370 @@
+/*
+ * 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.trace;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import io.opencensus.common.Scope;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.SpanBuilder.NoopSpanBuilder;
+import java.util.concurrent.Callable;
+import javax.annotation.Nullable;
+
+/**
+ * Tracer is a simple, thin class for {@link Span} creation and in-process context interaction.
+ *
+ * <p>Users may choose to use manual or automatic Context propagation. Because of that this class
+ * offers APIs to facilitate both usages.
+ *
+ * <p>The automatic context propagation is done using {@link io.grpc.Context} which is a gRPC
+ * independent implementation for in-process Context propagation mechanism which can carry
+ * scoped-values across API boundaries and between threads. Users of the library must propagate the
+ * {@link io.grpc.Context} between different threads.
+ *
+ * <p>Example usage with automatic context propagation:
+ *
+ * <pre>{@code
+ * class MyClass {
+ * private static final Tracer tracer = Tracing.getTracer();
+ * void doWork() {
+ * try(Scope ss = tracer.spanBuilder("MyClass.DoWork").startScopedSpan()) {
+ * tracer.getCurrentSpan().addAnnotation("Starting the work.");
+ * doWorkInternal();
+ * tracer.getCurrentSpan().addAnnotation("Finished working.");
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Example usage with manual context propagation:
+ *
+ * <pre>{@code
+ * class MyClass {
+ * private static final Tracer tracer = Tracing.getTracer();
+ * void doWork(Span parent) {
+ * Span childSpan = tracer.spanBuilderWithExplicitParent("MyChildSpan", parent).startSpan();
+ * childSpan.addAnnotation("Starting the work.");
+ * try {
+ * doSomeWork(childSpan); // Manually propagate the new span down the stack.
+ * } finally {
+ * // To make sure we end the span even in case of an exception.
+ * childSpan.end(); // Manually end the span.
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * @since 0.5
+ */
+public abstract class Tracer {
+ private static final NoopTracer noopTracer = new NoopTracer();
+
+ /**
+ * Returns the no-op implementation of the {@code Tracer}.
+ *
+ * @return the no-op implementation of the {@code Tracer}.
+ */
+ static Tracer getNoopTracer() {
+ return noopTracer;
+ }
+
+ /**
+ * Gets the current Span from the current Context.
+ *
+ * <p>To install a {@link Span} to the current Context use {@link #withSpan(Span)} OR use {@link
+ * SpanBuilder#startScopedSpan} methods to start a new {@code Span}.
+ *
+ * <p>startSpan methods do NOT modify the current Context {@code Span}.
+ *
+ * @return a default {@code Span} that does nothing and has an invalid {@link SpanContext} if no
+ * {@code Span} is associated with the current Context, otherwise the current {@code Span}
+ * from the Context.
+ * @since 0.5
+ */
+ public final Span getCurrentSpan() {
+ Span currentSpan = CurrentSpanUtils.getCurrentSpan();
+ return currentSpan != null ? currentSpan : BlankSpan.INSTANCE;
+ }
+
+ /**
+ * Enters the scope of code where the given {@link Span} is in the current Context, and returns an
+ * object that represents that scope. The scope is exited when the returned object is closed.
+ *
+ * <p>Supports try-with-resource idiom.
+ *
+ * <p>Can be called with {@link BlankSpan} to enter a scope of code where tracing is stopped.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * private static Tracer tracer = Tracing.getTracer();
+ * void doWork() {
+ * // Create a Span as a child of the current Span.
+ * Span span = tracer.spanBuilder("my span").startSpan();
+ * try (Scope ws = tracer.withSpan(span)) {
+ * tracer.getCurrentSpan().addAnnotation("my annotation");
+ * doSomeOtherWork(); // Here "span" is the current Span.
+ * }
+ * span.end();
+ * }
+ * }</pre>
+ *
+ * <p>Prior to Java SE 7, you can use a finally block to ensure that a resource is closed
+ * regardless of whether the try statement completes normally or abruptly.
+ *
+ * <p>Example of usage prior to Java SE7:
+ *
+ * <pre>{@code
+ * private static Tracer tracer = Tracing.getTracer();
+ * void doWork() {
+ * // Create a Span as a child of the current Span.
+ * Span span = tracer.spanBuilder("my span").startSpan();
+ * Scope ws = tracer.withSpan(span);
+ * try {
+ * tracer.getCurrentSpan().addAnnotation("my annotation");
+ * doSomeOtherWork(); // Here "span" is the current Span.
+ * } finally {
+ * ws.close();
+ * }
+ * span.end();
+ * }
+ * }</pre>
+ *
+ * @param span The {@link Span} to be set to the current Context.
+ * @return an object that defines a scope where the given {@link Span} will be set to the current
+ * Context.
+ * @throws NullPointerException if {@code span} is {@code null}.
+ * @since 0.5
+ */
+ @MustBeClosed
+ public final Scope withSpan(Span span) {
+ return CurrentSpanUtils.withSpan(Utils.checkNotNull(span, "span"), /* endSpan= */ false);
+ }
+
+ /**
+ * Returns a {@link Runnable} that runs the given task with the given {@code Span} in the current
+ * context.
+ *
+ * <p>Users may consider to use {@link SpanBuilder#startSpanAndRun(Runnable)}.
+ *
+ * <p>Any error will end up as a {@link Status#UNKNOWN}.
+ *
+ * <p>IMPORTANT: Caller must manually propagate the entire {@code io.grpc.Context} when wraps a
+ * {@code Runnable}, see the examples.
+ *
+ * <p>IMPORTANT: Caller must manually end the {@code Span} within the {@code Runnable}, or after
+ * the {@code Runnable} is executed.
+ *
+ * <p>Example with Executor wrapped with {@link io.grpc.Context#currentContextExecutor}:
+ *
+ * <pre><code>
+ * class MyClass {
+ * private static Tracer tracer = Tracing.getTracer();
+ * void handleRequest(Executor executor) {
+ * Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+ * executor.execute(tracer.withSpan(span, new Runnable() {
+ * {@literal @}Override
+ * public void run() {
+ * try {
+ * sendResult();
+ * } finally {
+ * span.end();
+ * }
+ * }
+ * }));
+ * }
+ * }
+ * </code></pre>
+ *
+ * <p>Example without Executor wrapped with {@link io.grpc.Context#currentContextExecutor}:
+ *
+ * <pre><code>
+ * class MyClass {
+ * private static Tracer tracer = Tracing.getTracer();
+ * void handleRequest(Executor executor) {
+ * Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+ * executor.execute(Context.wrap(tracer.withSpan(span, new Runnable() {
+ * {@literal @}Override
+ * public void run() {
+ * try {
+ * sendResult();
+ * } finally {
+ * span.end();
+ * }
+ * }
+ * })));
+ * }
+ * }
+ * </code></pre>
+ *
+ * @param span the {@code Span} to be set as current.
+ * @param runnable the {@code Runnable} to withSpan in the {@code Span}.
+ * @return the {@code Runnable}.
+ * @since 0.11.0
+ */
+ public final Runnable withSpan(Span span, Runnable runnable) {
+ return CurrentSpanUtils.withSpan(span, /* endSpan= */ false, runnable);
+ }
+
+ /**
+ * Returns a {@link Callable} that runs the given task with the given {@code Span} in the current
+ * context.
+ *
+ * <p>Users may consider to use {@link SpanBuilder#startSpanAndCall(Callable)}.
+ *
+ * <p>Any error will end up as a {@link Status#UNKNOWN}.
+ *
+ * <p>IMPORTANT: Caller must manually propagate the entire {@code io.grpc.Context} when wraps a
+ * {@code Callable}, see the examples.
+ *
+ * <p>IMPORTANT: Caller must manually end the {@code Span} within the {@code Callable}, or after
+ * the {@code Callable} is executed.
+ *
+ * <p>Example with Executor wrapped with {@link io.grpc.Context#currentContextExecutor}:
+ *
+ * <pre><code>
+ * class MyClass {
+ * private static Tracer tracer = Tracing.getTracer();
+ * void handleRequest(Executor executor) {
+ * Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+ * executor.execute(tracer.withSpan(span, {@code new Callable<MyResult>()} {
+ * {@literal @}Override
+ * public MyResult call() throws Exception {
+ * try {
+ * return sendResult();
+ * } finally {
+ * span.end();
+ * }
+ * }
+ * }));
+ * }
+ * }
+ * </code></pre>
+ *
+ * <p>Example without Executor wrapped with {@link io.grpc.Context#currentContextExecutor}:
+ *
+ * <pre><code>
+ * class MyClass {
+ * private static Tracer tracer = Tracing.getTracer();
+ * void handleRequest(Executor executor) {
+ * Span span = tracer.spanBuilder("MyRunnableSpan").startSpan();
+ * executor.execute(Context.wrap(tracer.withSpan(span, {@code new Callable<MyResult>()} {
+ * {@literal @}Override
+ * public MyResult call() throws Exception {
+ * try {
+ * return sendResult();
+ * } finally {
+ * span.end();
+ * }
+ * }
+ * })));
+ * }
+ * }
+ * </code></pre>
+ *
+ * @param span the {@code Span} to be set as current.
+ * @param callable the {@code Callable} to run in the {@code Span}.
+ * @return the {@code Callable}.
+ * @since 0.11.0
+ */
+ public final <C> Callable<C> withSpan(Span span, final Callable<C> callable) {
+ return CurrentSpanUtils.withSpan(span, /* endSpan= */ false, callable);
+ }
+
+ /**
+ * Returns a {@link SpanBuilder} to create and start a new child {@link Span} as a child of to the
+ * current {@code Span} if any, otherwise creates a root {@code Span}.
+ *
+ * <p>See {@link SpanBuilder} for usage examples.
+ *
+ * <p>This <b>must</b> be used to create a {@code Span} when automatic Context propagation is
+ * used.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * tracer.spanBuilderWithExplicitParent("MySpanName",tracer.getCurrentSpan());
+ * }</pre>
+ *
+ * @param spanName The name of the returned Span.
+ * @return a {@code SpanBuilder} to create and start a new {@code Span}.
+ * @throws NullPointerException if {@code spanName} is {@code null}.
+ * @since 0.5
+ */
+ public final SpanBuilder spanBuilder(String spanName) {
+ return spanBuilderWithExplicitParent(spanName, CurrentSpanUtils.getCurrentSpan());
+ }
+
+ /**
+ * Returns a {@link SpanBuilder} to create and start a new child {@link Span} (or root if parent
+ * is {@code null} or has an invalid {@link SpanContext}), with parent being the designated {@code
+ * Span}.
+ *
+ * <p>See {@link SpanBuilder} for usage examples.
+ *
+ * <p>This <b>must</b> be used to create a {@code Span} when manual Context propagation is used OR
+ * when creating a root {@code Span} with a {@code null} parent.
+ *
+ * @param spanName The name of the returned Span.
+ * @param parent The parent of the returned Span. If {@code null} the {@code SpanBuilder} will
+ * build a root {@code Span}.
+ * @return a {@code SpanBuilder} to create and start a new {@code Span}.
+ * @throws NullPointerException if {@code spanName} is {@code null}.
+ * @since 0.5
+ */
+ public abstract SpanBuilder spanBuilderWithExplicitParent(String spanName, @Nullable Span parent);
+
+ /**
+ * Returns a {@link SpanBuilder} to create and start a new child {@link Span} (or root if parent
+ * is {@link SpanContext#INVALID} or {@code null}), with parent being the remote {@link Span}
+ * designated by the {@link SpanContext}.
+ *
+ * <p>See {@link SpanBuilder} for usage examples.
+ *
+ * <p>This <b>must</b> be used to create a {@code Span} when the parent is in a different process.
+ * This is only intended for use by RPC systems or similar.
+ *
+ * <p>If no {@link SpanContext} OR fail to parse the {@link SpanContext} on the server side, users
+ * must call this method with a {@code null} remote parent {@code SpanContext}.
+ *
+ * @param spanName The name of the returned Span.
+ * @param remoteParentSpanContext The remote parent of the returned Span.
+ * @return a {@code SpanBuilder} to create and start a new {@code Span}.
+ * @throws NullPointerException if {@code spanName} is {@code null}.
+ * @since 0.5
+ */
+ public abstract SpanBuilder spanBuilderWithRemoteParent(
+ String spanName, @Nullable SpanContext remoteParentSpanContext);
+
+ // No-Op implementation of the Tracer.
+ private static final class NoopTracer extends Tracer {
+
+ @Override
+ public SpanBuilder spanBuilderWithExplicitParent(String spanName, @Nullable Span parent) {
+ return NoopSpanBuilder.createWithParent(spanName, parent);
+ }
+
+ @Override
+ public SpanBuilder spanBuilderWithRemoteParent(
+ String spanName, @Nullable SpanContext remoteParentSpanContext) {
+ return NoopSpanBuilder.createWithRemoteParent(spanName, remoteParentSpanContext);
+ }
+
+ private NoopTracer() {}
+ }
+
+ protected Tracer() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/Tracestate.java b/api/src/main/java/io/opencensus/trace/Tracestate.java
new file mode 100644
index 00000000..dae587c8
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Tracestate.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Carries tracing-system specific context in a list of key-value pairs. TraceState allows different
+ * vendors propagate additional information and inter-operate with their legacy Id formats.
+ *
+ * <p>Implementation is optimized for a small list of key-value pairs.
+ *
+ * <p>Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter,
+ * and can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and
+ * forward slashes /.
+ *
+ * <p>Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the
+ * range 0x20 to 0x7E) except comma , and =.
+ *
+ * @since 0.16
+ */
+@Immutable
+@AutoValue
+@ExperimentalApi
+public abstract class Tracestate {
+ private static final int KEY_MAX_SIZE = 256;
+ private static final int VALUE_MAX_SIZE = 256;
+ private static final int MAX_KEY_VALUE_PAIRS = 32;
+
+ /**
+ * Returns the value to which the specified key is mapped, or null if this map contains no mapping
+ * for the key.
+ *
+ * @param key with which the specified value is to be associated
+ * @return the value to which the specified key is mapped, or null if this map contains no mapping
+ * for the key.
+ * @since 0.16
+ */
+ @javax.annotation.Nullable
+ public String get(String key) {
+ for (Entry entry : getEntries()) {
+ if (entry.getKey().equals(key)) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a {@link List} view of the mappings contained in this {@code TraceState}.
+ *
+ * @return a {@link List} view of the mappings contained in this {@code TraceState}.
+ * @since 0.16
+ */
+ public abstract List<Entry> getEntries();
+
+ /**
+ * Returns a {@code Builder} based on an empty {@code Tracestate}.
+ *
+ * @return a {@code Builder} based on an empty {@code Tracestate}.
+ * @since 0.16
+ */
+ public static Builder builder() {
+ return new Builder(Builder.EMPTY);
+ }
+
+ /**
+ * Returns a {@code Builder} based on this {@code Tracestate}.
+ *
+ * @return a {@code Builder} based on this {@code Tracestate}.
+ * @since 0.16
+ */
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /**
+ * Builder class for {@link MessageEvent}.
+ *
+ * @since 0.16
+ */
+ @ExperimentalApi
+ public static final class Builder {
+ private final Tracestate parent;
+ @javax.annotation.Nullable private ArrayList<Entry> entries;
+
+ // Needs to be in this class to avoid initialization deadlock because super class depends on
+ // subclass (the auto-value generate class).
+ private static final Tracestate EMPTY = create(Collections.<Entry>emptyList());
+
+ private Builder(Tracestate parent) {
+ Utils.checkNotNull(parent, "parent");
+ this.parent = parent;
+ this.entries = null;
+ }
+
+ /**
+ * Adds or updates the {@code Entry} that has the given {@code key} if it is present. The new
+ * {@code Entry} will always be added in the front of the list of entries.
+ *
+ * @param key the key for the {@code Entry} to be added.
+ * @param value the value for the {@code Entry} to be added.
+ * @return this.
+ * @since 0.16
+ */
+ @SuppressWarnings("nullness")
+ public Builder set(String key, String value) {
+ // Initially create the Entry to validate input.
+ Entry entry = Entry.create(key, value);
+ if (entries == null) {
+ // Copy entries from the parent.
+ entries = new ArrayList<Entry>(parent.getEntries());
+ }
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).getKey().equals(entry.getKey())) {
+ entries.remove(i);
+ // Exit now because the entries list cannot contain duplicates.
+ break;
+ }
+ }
+ // Inserts the element at the front of this list.
+ entries.add(0, entry);
+ return this;
+ }
+
+ /**
+ * Removes the {@code Entry} that has the given {@code key} if it is present.
+ *
+ * @param key the key for the {@code Entry} to be removed.
+ * @return this.
+ * @since 0.16
+ */
+ @SuppressWarnings("nullness")
+ public Builder remove(String key) {
+ Utils.checkNotNull(key, "key");
+ if (entries == null) {
+ // Copy entries from the parent.
+ entries = new ArrayList<Entry>(parent.getEntries());
+ }
+ for (int i = 0; i < entries.size(); i++) {
+ if (entries.get(i).getKey().equals(key)) {
+ entries.remove(i);
+ // Exit now because the entries list cannot contain duplicates.
+ break;
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Builds a TraceState by adding the entries to the parent in front of the key-value pairs list
+ * and removing duplicate entries.
+ *
+ * @return a TraceState with the new entries.
+ * @since 0.16
+ */
+ public Tracestate build() {
+ if (entries == null) {
+ return parent;
+ }
+ return Tracestate.create(entries);
+ }
+ }
+
+ /**
+ * Immutable key-value pair for {@code Tracestate}.
+ *
+ * @since 0.16
+ */
+ @Immutable
+ @AutoValue
+ @ExperimentalApi
+ public abstract static class Entry {
+ /**
+ * Creates a new {@code Entry} for the {@code Tracestate}.
+ *
+ * @param key the Entry's key.
+ * @param value the Entry's value.
+ * @since 0.16
+ */
+ public static Entry create(String key, String value) {
+ Utils.checkNotNull(key, "key");
+ Utils.checkNotNull(value, "value");
+ Utils.checkArgument(validateKey(key), "Invalid key %s", key);
+ Utils.checkArgument(validateValue(value), "Invalid value %s", value);
+ return new AutoValue_Tracestate_Entry(key, value);
+ }
+
+ /**
+ * Returns the key {@code String}.
+ *
+ * @return the key {@code String}.
+ * @since 0.16
+ */
+ public abstract String getKey();
+
+ /**
+ * Returns the value {@code String}.
+ *
+ * @return the value {@code String}.
+ * @since 0.16
+ */
+ public abstract String getValue();
+
+ Entry() {}
+ }
+
+ // Key is opaque string up to 256 characters printable. It MUST begin with a lowercase letter, and
+ // can only contain lowercase letters a-z, digits 0-9, underscores _, dashes -, asterisks *, and
+ // forward slashes /.
+ private static boolean validateKey(String key) {
+ if (key.length() > KEY_MAX_SIZE
+ || key.isEmpty()
+ || key.charAt(0) < 'a'
+ || key.charAt(0) > 'z') {
+ return false;
+ }
+ for (int i = 1; i < key.length(); i++) {
+ char c = key.charAt(i);
+ if (!(c >= 'a' && c <= 'z')
+ && !(c >= '0' && c <= '9')
+ && c != '_'
+ && c != '-'
+ && c != '*'
+ && c != '/') {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Value is opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the range
+ // 0x20 to 0x7E) except comma , and =.
+ private static boolean validateValue(String value) {
+ if (value.length() > VALUE_MAX_SIZE || value.charAt(value.length() - 1) == ' ' /* '\u0020' */) {
+ return false;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == ',' || c == '=' || c < ' ' /* '\u0020' */ || c > '~' /* '\u007E' */) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static Tracestate create(List<Entry> entries) {
+ Utils.checkState(entries.size() <= MAX_KEY_VALUE_PAIRS, "Invalid size");
+ return new AutoValue_Tracestate(Collections.unmodifiableList(entries));
+ }
+
+ Tracestate() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/Tracing.java b/api/src/main/java/io/opencensus/trace/Tracing.java
new file mode 100644
index 00000000..f55cd775
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/Tracing.java
@@ -0,0 +1,125 @@
+/*
+ * 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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Provider;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/**
+ * Class that manages a global instance of the {@link TraceComponent}.
+ *
+ * @since 0.5
+ */
+public final class Tracing {
+ private static final Logger logger = Logger.getLogger(Tracing.class.getName());
+ private static final TraceComponent traceComponent =
+ loadTraceComponent(TraceComponent.class.getClassLoader());
+
+ /**
+ * Returns the global {@link Tracer}.
+ *
+ * @return the global {@code Tracer}.
+ * @since 0.5
+ */
+ public static Tracer getTracer() {
+ return traceComponent.getTracer();
+ }
+
+ /**
+ * Returns the global {@link PropagationComponent}.
+ *
+ * @return the global {@code PropagationComponent}.
+ * @since 0.5
+ */
+ public static PropagationComponent getPropagationComponent() {
+ return traceComponent.getPropagationComponent();
+ }
+
+ /**
+ * Returns the global {@link Clock}.
+ *
+ * @return the global {@code Clock}.
+ * @since 0.5
+ */
+ public static Clock getClock() {
+ return traceComponent.getClock();
+ }
+
+ /**
+ * Returns the global {@link ExportComponent}.
+ *
+ * @return the global {@code ExportComponent}.
+ * @since 0.5
+ */
+ public static ExportComponent getExportComponent() {
+ return traceComponent.getExportComponent();
+ }
+
+ /**
+ * Returns the global {@link TraceConfig}.
+ *
+ * @return the global {@code TraceConfig}.
+ * @since 0.5
+ */
+ public static TraceConfig getTraceConfig() {
+ return traceComponent.getTraceConfig();
+ }
+
+ // Any provider that may be used for TraceComponent can be added here.
+ @DefaultVisibilityForTesting
+ static TraceComponent loadTraceComponent(@Nullable ClassLoader classLoader) {
+ try {
+ // Call Class.forName with literal string name of the class to help shading tools.
+ return Provider.createInstance(
+ Class.forName(
+ "io.opencensus.impl.trace.TraceComponentImpl", /*initialize=*/ true, classLoader),
+ TraceComponent.class);
+ } catch (ClassNotFoundException e) {
+ logger.log(
+ Level.FINE,
+ "Couldn't load full implementation for TraceComponent, now trying to load lite "
+ + "implementation.",
+ e);
+ }
+ try {
+ // Call Class.forName with literal string name of the class to help shading tools.
+ return Provider.createInstance(
+ Class.forName(
+ "io.opencensus.impllite.trace.TraceComponentImplLite",
+ /*initialize=*/ true,
+ classLoader),
+ TraceComponent.class);
+ } catch (ClassNotFoundException e) {
+ logger.log(
+ Level.FINE,
+ "Couldn't load lite implementation for TraceComponent, now using "
+ + "default implementation for TraceComponent.",
+ e);
+ }
+ return TraceComponent.newNoopTraceComponent();
+ }
+
+ // No instance of this class.
+ private Tracing() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/config/TraceConfig.java b/api/src/main/java/io/opencensus/trace/config/TraceConfig.java
new file mode 100644
index 00000000..ff701e20
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/config/TraceConfig.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.trace.config;
+
+/**
+ * Global configuration of the trace service. This allows users to change configs for the default
+ * sampler, maximum events to be kept, etc. (see {@link TraceParams} for details).
+ *
+ * @since 0.5
+ */
+public abstract class TraceConfig {
+ private static final NoopTraceConfig NOOP_TRACE_CONFIG = new NoopTraceConfig();
+
+ /**
+ * Returns the active {@code TraceParams}.
+ *
+ * @return the active {@code TraceParams}.
+ * @since 0.5
+ */
+ public abstract TraceParams getActiveTraceParams();
+
+ /**
+ * Updates the active {@link TraceParams}.
+ *
+ * @param traceParams the new active {@code TraceParams}.
+ * @since 0.5
+ */
+ public abstract void updateActiveTraceParams(TraceParams traceParams);
+
+ /**
+ * Returns the no-op implementation of the {@code TraceConfig}.
+ *
+ * @return the no-op implementation of the {@code TraceConfig}.
+ * @since 0.5
+ */
+ public static TraceConfig getNoopTraceConfig() {
+ return NOOP_TRACE_CONFIG;
+ }
+
+ private static final class NoopTraceConfig extends TraceConfig {
+
+ @Override
+ public TraceParams getActiveTraceParams() {
+ return TraceParams.DEFAULT;
+ }
+
+ @Override
+ public void updateActiveTraceParams(TraceParams traceParams) {}
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/config/TraceParams.java b/api/src/main/java/io/opencensus/trace/config/TraceParams.java
new file mode 100644
index 00000000..ff70f52d
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/config/TraceParams.java
@@ -0,0 +1,219 @@
+/*
+ * 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.trace.config;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.samplers.Samplers;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Class that holds global trace parameters.
+ *
+ * @since 0.5
+ */
+@AutoValue
+@Immutable
+public abstract class TraceParams {
+ // These values are the default values for all the global parameters.
+ private static final double DEFAULT_PROBABILITY = 1e-4;
+ private static final Sampler DEFAULT_SAMPLER = Samplers.probabilitySampler(DEFAULT_PROBABILITY);
+ private static final int DEFAULT_SPAN_MAX_NUM_ATTRIBUTES = 32;
+ private static final int DEFAULT_SPAN_MAX_NUM_ANNOTATIONS = 32;
+ private static final int DEFAULT_SPAN_MAX_NUM_MESSAGE_EVENTS = 128;
+ private static final int DEFAULT_SPAN_MAX_NUM_LINKS = 32;
+
+ /**
+ * Default {@code TraceParams}.
+ *
+ * @since 0.5
+ */
+ public static final TraceParams DEFAULT =
+ TraceParams.builder()
+ .setSampler(DEFAULT_SAMPLER)
+ .setMaxNumberOfAttributes(DEFAULT_SPAN_MAX_NUM_ATTRIBUTES)
+ .setMaxNumberOfAnnotations(DEFAULT_SPAN_MAX_NUM_ANNOTATIONS)
+ .setMaxNumberOfMessageEvents(DEFAULT_SPAN_MAX_NUM_MESSAGE_EVENTS)
+ .setMaxNumberOfLinks(DEFAULT_SPAN_MAX_NUM_LINKS)
+ .build();
+
+ /**
+ * Returns the global default {@code Sampler}. Used if no {@code Sampler} is provided in {@link
+ * io.opencensus.trace.SpanBuilder#setSampler(Sampler)}.
+ *
+ * @return the global default {@code Sampler}.
+ * @since 0.5
+ */
+ public abstract Sampler getSampler();
+
+ /**
+ * Returns the global default max number of attributes per {@link Span}.
+ *
+ * @return the global default max number of attributes per {@link Span}.
+ * @since 0.5
+ */
+ public abstract int getMaxNumberOfAttributes();
+
+ /**
+ * Returns the global default max number of {@link Annotation} events per {@link Span}.
+ *
+ * @return the global default max number of {@code Annotation} events per {@code Span}.
+ * @since 0.5
+ */
+ public abstract int getMaxNumberOfAnnotations();
+
+ /**
+ * Returns the global default max number of {@link MessageEvent} events per {@link Span}.
+ *
+ * @return the global default max number of {@code MessageEvent} events per {@code Span}.
+ * @since 0.12
+ */
+ public abstract int getMaxNumberOfMessageEvents();
+
+ /**
+ * Returns the global default max number of {@link io.opencensus.trace.NetworkEvent} events per
+ * {@link Span}.
+ *
+ * @return the global default max number of {@code NetworkEvent} events per {@code Span}.
+ * @deprecated Use {@link getMaxNumberOfMessageEvents}.
+ * @since 0.5
+ */
+ @Deprecated
+ public int getMaxNumberOfNetworkEvents() {
+ return getMaxNumberOfMessageEvents();
+ }
+
+ /**
+ * Returns the global default max number of {@link Link} entries per {@link Span}.
+ *
+ * @return the global default max number of {@code Link} entries per {@code Span}.
+ * @since 0.5
+ */
+ public abstract int getMaxNumberOfLinks();
+
+ private static Builder builder() {
+ return new AutoValue_TraceParams.Builder();
+ }
+
+ /**
+ * Returns a {@link Builder} initialized to the same property values as the current instance.
+ *
+ * @return a {@link Builder} initialized to the same property values as the current instance.
+ * @since 0.5
+ */
+ public abstract Builder toBuilder();
+
+ /**
+ * A {@code Builder} class for {@link TraceParams}.
+ *
+ * @since 0.5
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ /**
+ * Sets the global default {@code Sampler}. It must be not {@code null} otherwise {@link
+ * #build()} will throw an exception.
+ *
+ * @param sampler the global default {@code Sampler}.
+ * @return this.
+ * @since 0.5
+ */
+ public abstract Builder setSampler(Sampler sampler);
+
+ /**
+ * Sets the global default max number of attributes per {@link Span}.
+ *
+ * @param maxNumberOfAttributes the global default max number of attributes per {@link Span}. It
+ * must be positive otherwise {@link #build()} will throw an exception.
+ * @return this.
+ * @since 0.5
+ */
+ public abstract Builder setMaxNumberOfAttributes(int maxNumberOfAttributes);
+
+ /**
+ * Sets the global default max number of {@link Annotation} events per {@link Span}.
+ *
+ * @param maxNumberOfAnnotations the global default max number of {@link Annotation} events per
+ * {@link Span}. It must be positive otherwise {@link #build()} will throw an exception.
+ * @return this.
+ * @since 0.5
+ */
+ public abstract Builder setMaxNumberOfAnnotations(int maxNumberOfAnnotations);
+
+ /**
+ * Sets the global default max number of {@link MessageEvent} events per {@link Span}.
+ *
+ * @param maxNumberOfMessageEvents the global default max number of {@link MessageEvent} events
+ * per {@link Span}. It must be positive otherwise {@link #build()} will throw an exception.
+ * @since 0.12
+ * @return this.
+ */
+ public abstract Builder setMaxNumberOfMessageEvents(int maxNumberOfMessageEvents);
+
+ /**
+ * Sets the global default max number of {@link io.opencensus.trace.NetworkEvent} events per
+ * {@link Span}.
+ *
+ * @param maxNumberOfNetworkEvents the global default max number of {@link
+ * io.opencensus.trace.NetworkEvent} events per {@link Span}. It must be positive otherwise
+ * {@link #build()} will throw an exception.
+ * @return this.
+ * @deprecated Use {@link setMaxNumberOfMessageEvents}.
+ * @since 0.5
+ */
+ @Deprecated
+ public Builder setMaxNumberOfNetworkEvents(int maxNumberOfNetworkEvents) {
+ return setMaxNumberOfMessageEvents(maxNumberOfNetworkEvents);
+ }
+
+ /**
+ * Sets the global default max number of {@link Link} entries per {@link Span}.
+ *
+ * @param maxNumberOfLinks the global default max number of {@link Link} entries per {@link
+ * Span}. It must be positive otherwise {@link #build()} will throw an exception.
+ * @return this.
+ * @since 0.5
+ */
+ public abstract Builder setMaxNumberOfLinks(int maxNumberOfLinks);
+
+ abstract TraceParams autoBuild();
+
+ /**
+ * Builds and returns a {@code TraceParams} with the desired values.
+ *
+ * @return a {@code TraceParams} with the desired values.
+ * @throws NullPointerException if the sampler is {@code null}.
+ * @throws IllegalArgumentException if any of the max numbers are not positive.
+ * @since 0.5
+ */
+ public TraceParams build() {
+ TraceParams traceParams = autoBuild();
+ Utils.checkArgument(traceParams.getMaxNumberOfAttributes() > 0, "maxNumberOfAttributes");
+ Utils.checkArgument(traceParams.getMaxNumberOfAnnotations() > 0, "maxNumberOfAnnotations");
+ Utils.checkArgument(
+ traceParams.getMaxNumberOfMessageEvents() > 0, "maxNumberOfMessageEvents");
+ Utils.checkArgument(traceParams.getMaxNumberOfLinks() > 0, "maxNumberOfLinks");
+ return traceParams;
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/ExportComponent.java b/api/src/main/java/io/opencensus/trace/export/ExportComponent.java
new file mode 100644
index 00000000..c334c5a6
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/ExportComponent.java
@@ -0,0 +1,95 @@
+/*
+ * 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.trace.export;
+
+import io.opencensus.trace.TraceOptions;
+
+/**
+ * Class that holds the implementation instances for {@link SpanExporter}, {@link RunningSpanStore}
+ * and {@link SampledSpanStore}.
+ *
+ * <p>Unless otherwise noted all methods (on component) results are cacheable.
+ *
+ * @since 0.5
+ */
+public abstract class ExportComponent {
+
+ /**
+ * Returns the no-op implementation of the {@code ExportComponent}.
+ *
+ * @return the no-op implementation of the {@code ExportComponent}.
+ * @since 0.5
+ */
+ public static ExportComponent newNoopExportComponent() {
+ return new NoopExportComponent();
+ }
+
+ /**
+ * Returns the {@link SpanExporter} which can be used to register handlers to export all the spans
+ * that are part of a distributed sampled trace (see {@link TraceOptions#isSampled()}).
+ *
+ * @return the implementation of the {@code SpanExporter} or no-op if no implementation linked in
+ * the binary.
+ * @since 0.5
+ */
+ public abstract SpanExporter getSpanExporter();
+
+ /**
+ * Returns the {@link RunningSpanStore} that can be used to get useful debugging information about
+ * all the current active spans.
+ *
+ * @return the {@code RunningSpanStore}.
+ * @since 0.5
+ */
+ public abstract RunningSpanStore getRunningSpanStore();
+
+ /**
+ * Returns the {@link SampledSpanStore} that can be used to get useful debugging information, such
+ * as latency based sampled spans, error based sampled spans.
+ *
+ * @return the {@code SampledSpanStore}.
+ * @since 0.5
+ */
+ public abstract SampledSpanStore getSampledSpanStore();
+
+ /**
+ * Will shutdown this ExportComponent after flushing any pending spans.
+ *
+ * @since 0.14
+ */
+ public void shutdown() {}
+
+ private static final class NoopExportComponent extends ExportComponent {
+ private final SampledSpanStore noopSampledSpanStore =
+ SampledSpanStore.newNoopSampledSpanStore();
+
+ @Override
+ public SpanExporter getSpanExporter() {
+ return SpanExporter.getNoopSpanExporter();
+ }
+
+ @Override
+ public RunningSpanStore getRunningSpanStore() {
+ return RunningSpanStore.getNoopRunningSpanStore();
+ }
+
+ @Override
+ public SampledSpanStore getSampledSpanStore() {
+ return noopSampledSpanStore;
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/RunningSpanStore.java b/api/src/main/java/io/opencensus/trace/export/RunningSpanStore.java
new file mode 100644
index 00000000..fac3c855
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/RunningSpanStore.java
@@ -0,0 +1,201 @@
+/*
+ * 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.trace.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * This class allows users to access in-process information about all running spans.
+ *
+ * <p>The running spans tracking is available for all the spans with the option {@link
+ * io.opencensus.trace.Span.Options#RECORD_EVENTS}. This functionality allows users to debug stuck
+ * operations or long living operations.
+ *
+ * @since 0.5
+ */
+@ThreadSafe
+public abstract class RunningSpanStore {
+
+ private static final RunningSpanStore NOOP_RUNNING_SPAN_STORE = new NoopRunningSpanStore();
+
+ protected RunningSpanStore() {}
+
+ /**
+ * Returns the no-op implementation of the {@code RunningSpanStore}.
+ *
+ * @return the no-op implementation of the {@code RunningSpanStore}.
+ */
+ static RunningSpanStore getNoopRunningSpanStore() {
+ return NOOP_RUNNING_SPAN_STORE;
+ }
+
+ /**
+ * Returns the summary of all available data such, as number of running spans.
+ *
+ * @return the summary of all available data.
+ * @since 0.5
+ */
+ public abstract Summary getSummary();
+
+ /**
+ * Returns a list of running spans that match the {@code Filter}.
+ *
+ * @param filter used to filter the returned spans.
+ * @return a list of running spans that match the {@code Filter}.
+ * @since 0.5
+ */
+ public abstract Collection<SpanData> getRunningSpans(Filter filter);
+
+ /**
+ * The summary of all available data.
+ *
+ * @since 0.5
+ */
+ @AutoValue
+ @Immutable
+ public abstract static class Summary {
+
+ Summary() {}
+
+ /**
+ * Returns a new instance of {@code Summary}.
+ *
+ * @param perSpanNameSummary a map with summary for each span name.
+ * @return a new instance of {@code Summary}.
+ * @throws NullPointerException if {@code perSpanNameSummary} is {@code null}.
+ * @since 0.5
+ */
+ public static Summary create(Map<String, PerSpanNameSummary> perSpanNameSummary) {
+ return new AutoValue_RunningSpanStore_Summary(
+ Collections.unmodifiableMap(
+ new HashMap<String, PerSpanNameSummary>(
+ Utils.checkNotNull(perSpanNameSummary, "perSpanNameSummary"))));
+ }
+
+ /**
+ * Returns a map with summary of available data for each span name.
+ *
+ * @return a map with all the span names and the summary.
+ * @since 0.5
+ */
+ public abstract Map<String, PerSpanNameSummary> getPerSpanNameSummary();
+ }
+
+ /**
+ * Summary of all available data for a span name.
+ *
+ * @since 0.5
+ */
+ @AutoValue
+ @Immutable
+ public abstract static class PerSpanNameSummary {
+
+ PerSpanNameSummary() {}
+
+ /**
+ * Returns a new instance of {@code PerSpanNameSummary}.
+ *
+ * @param numRunningSpans the number of running spans.
+ * @return a new instance of {@code PerSpanNameSummary}.
+ * @throws IllegalArgumentException if {@code numRunningSpans} is negative.
+ * @since 0.5
+ */
+ public static PerSpanNameSummary create(int numRunningSpans) {
+ Utils.checkArgument(numRunningSpans >= 0, "Negative numRunningSpans.");
+ return new AutoValue_RunningSpanStore_PerSpanNameSummary(numRunningSpans);
+ }
+
+ /**
+ * Returns the number of running spans.
+ *
+ * @return the number of running spans.
+ * @since 0.5
+ */
+ public abstract int getNumRunningSpans();
+ }
+
+ /**
+ * Filter for running spans. Used to filter results returned by the {@link
+ * #getRunningSpans(Filter)} request.
+ *
+ * @since 0.5
+ */
+ @AutoValue
+ @Immutable
+ public abstract static class Filter {
+
+ Filter() {}
+
+ /**
+ * Returns a new instance of {@code Filter}.
+ *
+ * <p>Filters all the spans based on {@code spanName} and returns a maximum of {@code
+ * maxSpansToReturn}.
+ *
+ * @param spanName the name of the span.
+ * @param maxSpansToReturn the maximum number of results to be returned. {@code 0} means all.
+ * @return a new instance of {@code Filter}.
+ * @throws NullPointerException if {@code spanName} is {@code null}.
+ * @throws IllegalArgumentException if {@code maxSpansToReturn} is negative.
+ * @since 0.5
+ */
+ public static Filter create(String spanName, int maxSpansToReturn) {
+ Utils.checkArgument(maxSpansToReturn >= 0, "Negative maxSpansToReturn.");
+ return new AutoValue_RunningSpanStore_Filter(spanName, maxSpansToReturn);
+ }
+
+ /**
+ * Returns the span name.
+ *
+ * @return the span name.
+ * @since 0.5
+ */
+ public abstract String getSpanName();
+
+ /**
+ * Returns the maximum number of spans to be returned. {@code 0} means all.
+ *
+ * @return the maximum number of spans to be returned.
+ * @since 0.5
+ */
+ public abstract int getMaxSpansToReturn();
+ }
+
+ private static final class NoopRunningSpanStore extends RunningSpanStore {
+
+ private static final Summary EMPTY_SUMMARY =
+ Summary.create(Collections.<String, PerSpanNameSummary>emptyMap());
+
+ @Override
+ public Summary getSummary() {
+ return EMPTY_SUMMARY;
+ }
+
+ @Override
+ public Collection<SpanData> getRunningSpans(Filter filter) {
+ Utils.checkNotNull(filter, "filter");
+ return Collections.<SpanData>emptyList();
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java b/api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java
new file mode 100644
index 00000000..5d00a45d
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java
@@ -0,0 +1,525 @@
+/*
+ * 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.trace.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Status.CanonicalCode;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * This class allows users to access in-process information such as latency based sampled spans and
+ * error based sampled spans.
+ *
+ * <p>For all completed spans with the option {@link Span.Options#RECORD_EVENTS} the library can
+ * store samples based on latency for succeeded operations or based on error code for failed
+ * operations. To activate this, users MUST manually configure all the span names for which samples
+ * will be collected (see {@link #registerSpanNamesForCollection(Collection)}).
+ *
+ * @since 0.5
+ */
+@ThreadSafe
+public abstract class SampledSpanStore {
+
+ protected SampledSpanStore() {}
+
+ /**
+ * Returns a {@code SampledSpanStore} that maintains a set of span names, but always returns an
+ * empty list of {@link SpanData}.
+ *
+ * @return a {@code SampledSpanStore} that maintains a set of span names, but always returns an
+ * empty list of {@code SpanData}.
+ */
+ static SampledSpanStore newNoopSampledSpanStore() {
+ return new NoopSampledSpanStore();
+ }
+
+ /**
+ * Returns the summary of all available data, such as number of sampled spans in the latency based
+ * samples or error based samples.
+ *
+ * <p>Data available only for span names registered using {@link
+ * #registerSpanNamesForCollection(Collection)}.
+ *
+ * @return the summary of all available data.
+ * @since 0.5
+ */
+ public abstract Summary getSummary();
+
+ /**
+ * Returns a list of succeeded spans (spans with {@link Status} equal to {@link Status#OK}) that
+ * match the {@code filter}.
+ *
+ * <p>Latency based sampled spans are available only for span names registered using {@link
+ * #registerSpanNamesForCollection(Collection)}.
+ *
+ * @param filter used to filter the returned sampled spans.
+ * @return a list of succeeded spans that match the {@code filter}.
+ * @since 0.5
+ */
+ public abstract Collection<SpanData> getLatencySampledSpans(LatencyFilter filter);
+
+ /**
+ * Returns a list of failed spans (spans with {@link Status} other than {@link Status#OK}) that
+ * match the {@code filter}.
+ *
+ * <p>Error based sampled spans are available only for span names registered using {@link
+ * #registerSpanNamesForCollection(Collection)}.
+ *
+ * @param filter used to filter the returned sampled spans.
+ * @return a list of failed spans that match the {@code filter}.
+ * @since 0.5
+ */
+ public abstract Collection<SpanData> getErrorSampledSpans(ErrorFilter filter);
+
+ /**
+ * Appends a list of span names for which the library will collect latency based sampled spans and
+ * error based sampled spans.
+ *
+ * <p>If called multiple times the library keeps the list of unique span names from all the calls.
+ *
+ * @param spanNames list of span names for which the library will collect samples.
+ * @since 0.5
+ */
+ public abstract void registerSpanNamesForCollection(Collection<String> spanNames);
+
+ /**
+ * Removes a list of span names for which the library will collect latency based sampled spans and
+ * error based sampled spans.
+ *
+ * <p>The library keeps the list of unique registered span names for which samples will be called.
+ * This method allows users to remove span names from that list.
+ *
+ * @param spanNames list of span names for which the library will no longer collect samples.
+ * @since 0.5
+ */
+ public abstract void unregisterSpanNamesForCollection(Collection<String> spanNames);
+
+ /**
+ * Returns the set of unique span names registered to the library, for use in tests. For this set
+ * of span names the library will collect latency based sampled spans and error based sampled
+ * spans.
+ *
+ * <p>This method is only meant for testing code that uses OpenCensus, and it is not performant.
+ *
+ * @return the set of unique span names registered to the library.
+ * @since 0.7
+ */
+ public abstract Set<String> getRegisteredSpanNamesForCollection();
+
+ /**
+ * The summary of all available data.
+ *
+ * @since 0.5
+ */
+ @AutoValue
+ @Immutable
+ public abstract static class Summary {
+
+ Summary() {}
+
+ /**
+ * Returns a new instance of {@code Summary}.
+ *
+ * @param perSpanNameSummary a map with summary for each span name.
+ * @return a new instance of {@code Summary}.
+ * @throws NullPointerException if {@code perSpanNameSummary} is {@code null}.
+ * @since 0.5
+ */
+ public static Summary create(Map<String, PerSpanNameSummary> perSpanNameSummary) {
+ return new AutoValue_SampledSpanStore_Summary(
+ Collections.unmodifiableMap(
+ new HashMap<String, PerSpanNameSummary>(
+ Utils.checkNotNull(perSpanNameSummary, "perSpanNameSummary"))));
+ }
+
+ /**
+ * Returns a map with summary of available data for each span name.
+ *
+ * @return a map with all the span names and the summary.
+ * @since 0.5
+ */
+ public abstract Map<String, PerSpanNameSummary> getPerSpanNameSummary();
+ }
+
+ /**
+ * Summary of all available data for a span name.
+ *
+ * @since 0.5
+ */
+ @AutoValue
+ @Immutable
+ public abstract static class PerSpanNameSummary {
+
+ PerSpanNameSummary() {}
+
+ /**
+ * Returns a new instance of {@code PerSpanNameSummary}.
+ *
+ * @param numbersOfLatencySampledSpans the summary for the latency buckets.
+ * @param numbersOfErrorSampledSpans the summary for the error buckets.
+ * @return a new instance of {@code PerSpanNameSummary}.
+ * @throws NullPointerException if {@code numbersOfLatencySampledSpans} or {@code
+ * numbersOfErrorSampledSpans} are {@code null}.
+ * @since 0.5
+ */
+ public static PerSpanNameSummary create(
+ Map<LatencyBucketBoundaries, Integer> numbersOfLatencySampledSpans,
+ Map<CanonicalCode, Integer> numbersOfErrorSampledSpans) {
+ return new AutoValue_SampledSpanStore_PerSpanNameSummary(
+ Collections.unmodifiableMap(
+ new HashMap<LatencyBucketBoundaries, Integer>(
+ Utils.checkNotNull(
+ numbersOfLatencySampledSpans, "numbersOfLatencySampledSpans"))),
+ Collections.unmodifiableMap(
+ new HashMap<CanonicalCode, Integer>(
+ Utils.checkNotNull(numbersOfErrorSampledSpans, "numbersOfErrorSampledSpans"))));
+ }
+
+ /**
+ * Returns the number of sampled spans in all the latency buckets.
+ *
+ * <p>Data available only for span names registered using {@link
+ * #registerSpanNamesForCollection(Collection)}.
+ *
+ * @return the number of sampled spans in all the latency buckets.
+ * @since 0.5
+ */
+ public abstract Map<LatencyBucketBoundaries, Integer> getNumbersOfLatencySampledSpans();
+
+ /**
+ * Returns the number of sampled spans in all the error buckets.
+ *
+ * <p>Data available only for span names registered using {@link
+ * #registerSpanNamesForCollection(Collection)}.
+ *
+ * @return the number of sampled spans in all the error buckets.
+ * @since 0.5
+ */
+ public abstract Map<CanonicalCode, Integer> getNumbersOfErrorSampledSpans();
+ }
+
+ /**
+ * The latency buckets boundaries. Samples based on latency for successful spans (the status of
+ * the span has a canonical code equal to {@link CanonicalCode#OK}) are collected in one of these
+ * latency buckets.
+ *
+ * @since 0.5
+ */
+ public enum LatencyBucketBoundaries {
+ /**
+ * Stores finished successful requests of duration within the interval [0, 10us).
+ *
+ * @since 0.5
+ */
+ ZERO_MICROSx10(0, TimeUnit.MICROSECONDS.toNanos(10)),
+
+ /**
+ * Stores finished successful requests of duration within the interval [10us, 100us).
+ *
+ * @since 0.5
+ */
+ MICROSx10_MICROSx100(TimeUnit.MICROSECONDS.toNanos(10), TimeUnit.MICROSECONDS.toNanos(100)),
+
+ /**
+ * Stores finished successful requests of duration within the interval [100us, 1ms).
+ *
+ * @since 0.5
+ */
+ MICROSx100_MILLIx1(TimeUnit.MICROSECONDS.toNanos(100), TimeUnit.MILLISECONDS.toNanos(1)),
+
+ /**
+ * Stores finished successful requests of duration within the interval [1ms, 10ms).
+ *
+ * @since 0.5
+ */
+ MILLIx1_MILLIx10(TimeUnit.MILLISECONDS.toNanos(1), TimeUnit.MILLISECONDS.toNanos(10)),
+
+ /**
+ * Stores finished successful requests of duration within the interval [10ms, 100ms).
+ *
+ * @since 0.5
+ */
+ MILLIx10_MILLIx100(TimeUnit.MILLISECONDS.toNanos(10), TimeUnit.MILLISECONDS.toNanos(100)),
+
+ /**
+ * Stores finished successful requests of duration within the interval [100ms, 1sec).
+ *
+ * @since 0.5
+ */
+ MILLIx100_SECONDx1(TimeUnit.MILLISECONDS.toNanos(100), TimeUnit.SECONDS.toNanos(1)),
+
+ /**
+ * Stores finished successful requests of duration within the interval [1sec, 10sec).
+ *
+ * @since 0.5
+ */
+ SECONDx1_SECONDx10(TimeUnit.SECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10)),
+
+ /**
+ * Stores finished successful requests of duration within the interval [10sec, 100sec).
+ *
+ * @since 0.5
+ */
+ SECONDx10_SECONDx100(TimeUnit.SECONDS.toNanos(10), TimeUnit.SECONDS.toNanos(100)),
+
+ /**
+ * Stores finished successful requests of duration &gt;= 100sec.
+ *
+ * @since 0.5
+ */
+ SECONDx100_MAX(TimeUnit.SECONDS.toNanos(100), Long.MAX_VALUE);
+
+ /**
+ * Constructs a {@code LatencyBucketBoundaries} with the given boundaries and label.
+ *
+ * @param latencyLowerNs the latency lower bound of the bucket.
+ * @param latencyUpperNs the latency upper bound of the bucket.
+ */
+ LatencyBucketBoundaries(long latencyLowerNs, long latencyUpperNs) {
+ this.latencyLowerNs = latencyLowerNs;
+ this.latencyUpperNs = latencyUpperNs;
+ }
+
+ /**
+ * Returns the latency lower bound of the bucket.
+ *
+ * @return the latency lower bound of the bucket.
+ * @since 0.5
+ */
+ public long getLatencyLowerNs() {
+ return latencyLowerNs;
+ }
+
+ /**
+ * Returns the latency upper bound of the bucket.
+ *
+ * @return the latency upper bound of the bucket.
+ * @since 0.5
+ */
+ public long getLatencyUpperNs() {
+ return latencyUpperNs;
+ }
+
+ private final long latencyLowerNs;
+ private final long latencyUpperNs;
+ }
+
+ /**
+ * Filter for latency based sampled spans. Used to filter results returned by the {@link
+ * #getLatencySampledSpans(LatencyFilter)} request.
+ *
+ * @since 0.5
+ */
+ @AutoValue
+ @Immutable
+ public abstract static class LatencyFilter {
+
+ LatencyFilter() {}
+
+ /**
+ * Returns a new instance of {@code LatencyFilter}.
+ *
+ * <p>Filters all the spans based on {@code spanName} and latency in the interval
+ * [latencyLowerNs, latencyUpperNs) and returns a maximum of {@code maxSpansToReturn}.
+ *
+ * @param spanName the name of the span.
+ * @param latencyLowerNs the latency lower bound.
+ * @param latencyUpperNs the latency upper bound.
+ * @param maxSpansToReturn the maximum number of results to be returned. {@code 0} means all.
+ * @return a new instance of {@code LatencyFilter}.
+ * @throws NullPointerException if {@code spanName} is {@code null}.
+ * @throws IllegalArgumentException if {@code maxSpansToReturn} or {@code latencyLowerNs} or
+ * {@code latencyUpperNs} are negative.
+ * @since 0.5
+ */
+ public static LatencyFilter create(
+ String spanName, long latencyLowerNs, long latencyUpperNs, int maxSpansToReturn) {
+ Utils.checkArgument(maxSpansToReturn >= 0, "Negative maxSpansToReturn.");
+ Utils.checkArgument(latencyLowerNs >= 0, "Negative latencyLowerNs");
+ Utils.checkArgument(latencyUpperNs >= 0, "Negative latencyUpperNs");
+ return new AutoValue_SampledSpanStore_LatencyFilter(
+ spanName, latencyLowerNs, latencyUpperNs, maxSpansToReturn);
+ }
+
+ /**
+ * Returns the span name used by this filter.
+ *
+ * @return the span name used by this filter.
+ * @since 0.5
+ */
+ public abstract String getSpanName();
+
+ /**
+ * Returns the latency lower bound of this bucket (inclusive).
+ *
+ * @return the latency lower bound of this bucket.
+ * @since 0.5
+ */
+ public abstract long getLatencyLowerNs();
+
+ /**
+ * Returns the latency upper bound of this bucket (exclusive).
+ *
+ * @return the latency upper bound of this bucket.
+ * @since 0.5
+ */
+ public abstract long getLatencyUpperNs();
+
+ /**
+ * Returns the maximum number of spans to be returned. {@code 0} means all.
+ *
+ * @return the maximum number of spans to be returned.
+ * @since 0.5
+ */
+ public abstract int getMaxSpansToReturn();
+ }
+
+ /**
+ * Filter for error based sampled spans. Used to filter results returned by the {@link
+ * #getErrorSampledSpans(ErrorFilter)} request.
+ *
+ * @since 0.5
+ */
+ @AutoValue
+ @Immutable
+ public abstract static class ErrorFilter {
+
+ ErrorFilter() {}
+
+ /**
+ * Returns a new instance of {@code ErrorFilter}.
+ *
+ * <p>Filters all the spans based on {@code spanName} and {@code canonicalCode} and returns a
+ * maximum of {@code maxSpansToReturn}.
+ *
+ * @param spanName the name of the span.
+ * @param canonicalCode the error code of the span. {@code null} can be used to query all error
+ * codes.
+ * @param maxSpansToReturn the maximum number of results to be returned. {@code 0} means all.
+ * @return a new instance of {@code ErrorFilter}.
+ * @throws NullPointerException if {@code spanName} is {@code null}.
+ * @throws IllegalArgumentException if {@code canonicalCode} is {@link CanonicalCode#OK} or
+ * {@code maxSpansToReturn} is negative.
+ * @since 0.5
+ */
+ public static ErrorFilter create(
+ String spanName, @Nullable CanonicalCode canonicalCode, int maxSpansToReturn) {
+ if (canonicalCode != null) {
+ Utils.checkArgument(canonicalCode != CanonicalCode.OK, "Invalid canonical code.");
+ }
+ Utils.checkArgument(maxSpansToReturn >= 0, "Negative maxSpansToReturn.");
+ return new AutoValue_SampledSpanStore_ErrorFilter(spanName, canonicalCode, maxSpansToReturn);
+ }
+
+ /**
+ * Returns the span name used by this filter.
+ *
+ * @return the span name used by this filter.
+ * @since 0.5
+ */
+ public abstract String getSpanName();
+
+ /**
+ * Returns the canonical code used by this filter. Always different than {@link
+ * CanonicalCode#OK}. If {@code null} then all errors match.
+ *
+ * @return the canonical code used by this filter.
+ * @since 0.5
+ */
+ @Nullable
+ public abstract CanonicalCode getCanonicalCode();
+
+ /**
+ * Returns the maximum number of spans to be returned. Used to enforce the number of returned
+ * {@code SpanData}. {@code 0} means all.
+ *
+ * @return the maximum number of spans to be returned.
+ * @since 0.5
+ */
+ public abstract int getMaxSpansToReturn();
+ }
+
+ @ThreadSafe
+ private static final class NoopSampledSpanStore extends SampledSpanStore {
+ private static final PerSpanNameSummary EMPTY_PER_SPAN_NAME_SUMMARY =
+ PerSpanNameSummary.create(
+ Collections.<SampledSpanStore.LatencyBucketBoundaries, Integer>emptyMap(),
+ Collections.<CanonicalCode, Integer>emptyMap());
+
+ @GuardedBy("registeredSpanNames")
+ private final Set<String> registeredSpanNames = new HashSet<String>();
+
+ @Override
+ public Summary getSummary() {
+ Map<String, PerSpanNameSummary> result = new HashMap<String, PerSpanNameSummary>();
+ synchronized (registeredSpanNames) {
+ for (String registeredSpanName : registeredSpanNames) {
+ result.put(registeredSpanName, EMPTY_PER_SPAN_NAME_SUMMARY);
+ }
+ }
+ return Summary.create(result);
+ }
+
+ @Override
+ public Collection<SpanData> getLatencySampledSpans(LatencyFilter filter) {
+ Utils.checkNotNull(filter, "latencyFilter");
+ return Collections.<SpanData>emptyList();
+ }
+
+ @Override
+ public Collection<SpanData> getErrorSampledSpans(ErrorFilter filter) {
+ Utils.checkNotNull(filter, "errorFilter");
+ return Collections.<SpanData>emptyList();
+ }
+
+ @Override
+ public void registerSpanNamesForCollection(Collection<String> spanNames) {
+ Utils.checkNotNull(spanNames, "spanNames");
+ synchronized (registeredSpanNames) {
+ registeredSpanNames.addAll(spanNames);
+ }
+ }
+
+ @Override
+ public void unregisterSpanNamesForCollection(Collection<String> spanNames) {
+ Utils.checkNotNull(spanNames, "spanNames");
+ synchronized (registeredSpanNames) {
+ registeredSpanNames.removeAll(spanNames);
+ }
+ }
+
+ @Override
+ public Set<String> getRegisteredSpanNamesForCollection() {
+ synchronized (registeredSpanNames) {
+ return Collections.<String>unmodifiableSet(new HashSet<String>(registeredSpanNames));
+ }
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/SpanData.java b/api/src/main/java/io/opencensus/trace/export/SpanData.java
new file mode 100644
index 00000000..f4dd4682
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/SpanData.java
@@ -0,0 +1,477 @@
+/*
+ * 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.trace.export;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Timestamp;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.internal.BaseMessageEventUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/*>>>
+import org.checkerframework.dataflow.qual.Deterministic;
+*/
+
+/**
+ * Immutable representation of all data collected by the {@link Span} class.
+ *
+ * @since 0.5
+ */
+@Immutable
+@AutoValue
+public abstract class SpanData {
+
+ /**
+ * Returns a new immutable {@code SpanData}.
+ *
+ * @deprecated Use {@link #create(SpanContext, SpanId, Boolean, String, Kind, Timestamp,
+ * Attributes, TimedEvents, TimedEvents, Links, Integer, Status, Timestamp)}.
+ */
+ @Deprecated
+ public static SpanData create(
+ SpanContext context,
+ @Nullable SpanId parentSpanId,
+ @Nullable Boolean hasRemoteParent,
+ String name,
+ Timestamp startTimestamp,
+ Attributes attributes,
+ TimedEvents<Annotation> annotations,
+ TimedEvents<? extends io.opencensus.trace.BaseMessageEvent> messageOrNetworkEvents,
+ Links links,
+ @Nullable Integer childSpanCount,
+ @Nullable Status status,
+ @Nullable Timestamp endTimestamp) {
+ return create(
+ context,
+ parentSpanId,
+ hasRemoteParent,
+ name,
+ null,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageOrNetworkEvents,
+ links,
+ childSpanCount,
+ status,
+ endTimestamp);
+ }
+
+ /**
+ * Returns a new immutable {@code SpanData}.
+ *
+ * @param context the {@code SpanContext} of the {@code Span}.
+ * @param parentSpanId the parent {@code SpanId} of the {@code Span}. {@code null} if the {@code
+ * Span} is a root.
+ * @param hasRemoteParent {@code true} if the parent {@code Span} is remote. {@code null} if this
+ * is a root span.
+ * @param name the name of the {@code Span}.
+ * @param kind the kind of the {@code Span}.
+ * @param startTimestamp the start {@code Timestamp} of the {@code Span}.
+ * @param attributes the attributes associated with the {@code Span}.
+ * @param annotations the annotations associated with the {@code Span}.
+ * @param messageOrNetworkEvents the message events (or network events for backward compatibility)
+ * associated with the {@code Span}.
+ * @param links the links associated with the {@code Span}.
+ * @param childSpanCount the number of child spans that were generated while the span was active.
+ * @param status the {@code Status} of the {@code Span}. {@code null} if the {@code Span} is still
+ * active.
+ * @param endTimestamp the end {@code Timestamp} of the {@code Span}. {@code null} if the {@code
+ * Span} is still active.
+ * @return a new immutable {@code SpanData}.
+ * @since 0.14
+ */
+ @SuppressWarnings({"deprecation", "InconsistentOverloads"})
+ public static SpanData create(
+ SpanContext context,
+ @Nullable SpanId parentSpanId,
+ @Nullable Boolean hasRemoteParent,
+ String name,
+ @Nullable Kind kind,
+ Timestamp startTimestamp,
+ Attributes attributes,
+ TimedEvents<Annotation> annotations,
+ TimedEvents<? extends io.opencensus.trace.BaseMessageEvent> messageOrNetworkEvents,
+ Links links,
+ @Nullable Integer childSpanCount,
+ @Nullable Status status,
+ @Nullable Timestamp endTimestamp) {
+ Utils.checkNotNull(messageOrNetworkEvents, "messageOrNetworkEvents");
+ List<TimedEvent<MessageEvent>> messageEventsList = new ArrayList<TimedEvent<MessageEvent>>();
+ for (TimedEvent<? extends io.opencensus.trace.BaseMessageEvent> timedEvent :
+ messageOrNetworkEvents.getEvents()) {
+ io.opencensus.trace.BaseMessageEvent event = timedEvent.getEvent();
+ if (event instanceof MessageEvent) {
+ @SuppressWarnings("unchecked")
+ TimedEvent<MessageEvent> timedMessageEvent = (TimedEvent<MessageEvent>) timedEvent;
+ messageEventsList.add(timedMessageEvent);
+ } else {
+ messageEventsList.add(
+ TimedEvent.<MessageEvent>create(
+ timedEvent.getTimestamp(), BaseMessageEventUtils.asMessageEvent(event)));
+ }
+ }
+ TimedEvents<MessageEvent> messageEvents =
+ TimedEvents.<MessageEvent>create(
+ messageEventsList, messageOrNetworkEvents.getDroppedEventsCount());
+ return new AutoValue_SpanData(
+ context,
+ parentSpanId,
+ hasRemoteParent,
+ name,
+ kind,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ childSpanCount,
+ status,
+ endTimestamp);
+ }
+
+ /**
+ * Returns the {@code SpanContext} associated with this {@code Span}.
+ *
+ * @return the {@code SpanContext} associated with this {@code Span}.
+ * @since 0.5
+ */
+ public abstract SpanContext getContext();
+
+ /**
+ * Returns the parent {@code SpanId} or {@code null} if the {@code Span} is a root {@code Span}.
+ *
+ * @return the parent {@code SpanId} or {@code null} if the {@code Span} is a root {@code Span}.
+ * @since 0.5
+ */
+ @Nullable
+ /*@Deterministic*/
+ public abstract SpanId getParentSpanId();
+
+ /**
+ * Returns {@code true} if the parent is on a different process. {@code null} if this is a root
+ * span.
+ *
+ * @return {@code true} if the parent is on a different process. {@code null} if this is a root
+ * span.
+ * @since 0.5
+ */
+ @Nullable
+ public abstract Boolean getHasRemoteParent();
+
+ /**
+ * Returns the name of this {@code Span}.
+ *
+ * @return the name of this {@code Span}.
+ * @since 0.5
+ */
+ public abstract String getName();
+
+ /**
+ * Returns the kind of this {@code Span}.
+ *
+ * @return the kind of this {@code Span}.
+ * @since 0.14
+ */
+ @Nullable
+ public abstract Kind getKind();
+
+ /**
+ * Returns the start {@code Timestamp} of this {@code Span}.
+ *
+ * @return the start {@code Timestamp} of this {@code Span}.
+ * @since 0.5
+ */
+ public abstract Timestamp getStartTimestamp();
+
+ /**
+ * Returns the attributes recorded for this {@code Span}.
+ *
+ * @return the attributes recorded for this {@code Span}.
+ * @since 0.5
+ */
+ public abstract Attributes getAttributes();
+
+ /**
+ * Returns the annotations recorded for this {@code Span}.
+ *
+ * @return the annotations recorded for this {@code Span}.
+ * @since 0.5
+ */
+ public abstract TimedEvents<Annotation> getAnnotations();
+
+ /**
+ * Returns network events recorded for this {@code Span}.
+ *
+ * @return network events recorded for this {@code Span}.
+ * @deprecated Use {@link #getMessageEvents}.
+ * @since 0.5
+ */
+ @Deprecated
+ @SuppressWarnings({"deprecation"})
+ public TimedEvents<io.opencensus.trace.NetworkEvent> getNetworkEvents() {
+ TimedEvents<MessageEvent> timedEvents = getMessageEvents();
+ List<TimedEvent<io.opencensus.trace.NetworkEvent>> networkEventsList =
+ new ArrayList<TimedEvent<io.opencensus.trace.NetworkEvent>>();
+ for (TimedEvent<MessageEvent> timedEvent : timedEvents.getEvents()) {
+ networkEventsList.add(
+ TimedEvent.<io.opencensus.trace.NetworkEvent>create(
+ timedEvent.getTimestamp(),
+ BaseMessageEventUtils.asNetworkEvent(timedEvent.getEvent())));
+ }
+ return TimedEvents.<io.opencensus.trace.NetworkEvent>create(
+ networkEventsList, timedEvents.getDroppedEventsCount());
+ }
+
+ /**
+ * Returns message events recorded for this {@code Span}.
+ *
+ * @return message events recorded for this {@code Span}.
+ * @since 0.12
+ */
+ public abstract TimedEvents<MessageEvent> getMessageEvents();
+
+ /**
+ * Returns links recorded for this {@code Span}.
+ *
+ * @return links recorded for this {@code Span}.
+ * @since 0.5
+ */
+ public abstract Links getLinks();
+
+ /**
+ * Returns the number of child spans that were generated while the {@code Span} was running. If
+ * not {@code null} allows service implementations to detect missing child spans.
+ *
+ * <p>This information is not always available.
+ *
+ * @return the number of child spans that were generated while the {@code Span} was running.
+ * @since 0.5
+ */
+ @Nullable
+ public abstract Integer getChildSpanCount();
+
+ /**
+ * Returns the {@code Status} or {@code null} if {@code Span} is still active.
+ *
+ * @return the {@code Status} or {@code null} if {@code Span} is still active.
+ * @since 0.5
+ */
+ @Nullable
+ /*@Deterministic*/
+ public abstract Status getStatus();
+
+ /**
+ * Returns the end {@code Timestamp} or {@code null} if the {@code Span} is still active.
+ *
+ * @return the end {@code Timestamp} or {@code null} if the {@code Span} is still active.
+ * @since 0.5
+ */
+ @Nullable
+ /*@Deterministic*/
+ public abstract Timestamp getEndTimestamp();
+
+ SpanData() {}
+
+ /**
+ * A timed event representation.
+ *
+ * @param <T> the type of value that is timed.
+ * @since 0.5
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class TimedEvent<T> {
+ /**
+ * Returns a new immutable {@code TimedEvent<T>}.
+ *
+ * @param timestamp the {@code Timestamp} of this event.
+ * @param event the event.
+ * @param <T> the type of value that is timed.
+ * @return a new immutable {@code TimedEvent<T>}
+ * @since 0.5
+ */
+ public static <T> TimedEvent<T> create(Timestamp timestamp, T event) {
+ return new AutoValue_SpanData_TimedEvent<T>(timestamp, event);
+ }
+
+ /**
+ * Returns the {@code Timestamp} of this event.
+ *
+ * @return the {@code Timestamp} of this event.
+ * @since 0.5
+ */
+ public abstract Timestamp getTimestamp();
+
+ /**
+ * Returns the event.
+ *
+ * @return the event.
+ * @since 0.5
+ */
+ /*@Deterministic*/
+ public abstract T getEvent();
+
+ TimedEvent() {}
+ }
+
+ /**
+ * A list of timed events and the number of dropped events representation.
+ *
+ * @param <T> the type of value that is timed.
+ * @since 0.5
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class TimedEvents<T> {
+ /**
+ * Returns a new immutable {@code TimedEvents<T>}.
+ *
+ * @param events the list of events.
+ * @param droppedEventsCount the number of dropped events.
+ * @param <T> the type of value that is timed.
+ * @return a new immutable {@code TimedEvents<T>}
+ * @since 0.5
+ */
+ public static <T> TimedEvents<T> create(List<TimedEvent<T>> events, int droppedEventsCount) {
+ return new AutoValue_SpanData_TimedEvents<T>(
+ Collections.unmodifiableList(
+ new ArrayList<TimedEvent<T>>(Utils.checkNotNull(events, "events"))),
+ droppedEventsCount);
+ }
+
+ /**
+ * Returns the list of events.
+ *
+ * @return the list of events.
+ * @since 0.5
+ */
+ public abstract List<TimedEvent<T>> getEvents();
+
+ /**
+ * Returns the number of dropped events.
+ *
+ * @return the number of dropped events.
+ * @since 0.5
+ */
+ public abstract int getDroppedEventsCount();
+
+ TimedEvents() {}
+ }
+
+ /**
+ * A set of attributes and the number of dropped attributes representation.
+ *
+ * @since 0.5
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class Attributes {
+ /**
+ * Returns a new immutable {@code Attributes}.
+ *
+ * @param attributeMap the set of attributes.
+ * @param droppedAttributesCount the number of dropped attributes.
+ * @return a new immutable {@code Attributes}.
+ * @since 0.5
+ */
+ public static Attributes create(
+ Map<String, AttributeValue> attributeMap, int droppedAttributesCount) {
+ // TODO(bdrutu): Consider to use LinkedHashMap here and everywhere else, less test flakes
+ // for others on account of determinism.
+ return new AutoValue_SpanData_Attributes(
+ Collections.unmodifiableMap(
+ new HashMap<String, AttributeValue>(
+ Utils.checkNotNull(attributeMap, "attributeMap"))),
+ droppedAttributesCount);
+ }
+
+ /**
+ * Returns the set of attributes.
+ *
+ * @return the set of attributes.
+ * @since 0.5
+ */
+ public abstract Map<String, AttributeValue> getAttributeMap();
+
+ /**
+ * Returns the number of dropped attributes.
+ *
+ * @return the number of dropped attributes.
+ * @since 0.5
+ */
+ public abstract int getDroppedAttributesCount();
+
+ Attributes() {}
+ }
+
+ /**
+ * A list of links and the number of dropped links representation.
+ *
+ * @since 0.5
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class Links {
+ /**
+ * Returns a new immutable {@code Links}.
+ *
+ * @param links the list of links.
+ * @param droppedLinksCount the number of dropped links.
+ * @return a new immutable {@code Links}.
+ * @since 0.5
+ */
+ public static Links create(List<Link> links, int droppedLinksCount) {
+ return new AutoValue_SpanData_Links(
+ Collections.unmodifiableList(new ArrayList<Link>(Utils.checkNotNull(links, "links"))),
+ droppedLinksCount);
+ }
+
+ /**
+ * Returns the list of links.
+ *
+ * @return the list of links.
+ * @since 0.5
+ */
+ public abstract List<Link> getLinks();
+
+ /**
+ * Returns the number of dropped links.
+ *
+ * @return the number of dropped links.
+ * @since 0.5
+ */
+ public abstract int getDroppedLinksCount();
+
+ Links() {}
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/export/SpanExporter.java b/api/src/main/java/io/opencensus/trace/export/SpanExporter.java
new file mode 100644
index 00000000..73ac5265
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/export/SpanExporter.java
@@ -0,0 +1,96 @@
+/*
+ * 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.trace.export;
+
+import io.opencensus.trace.Span;
+import io.opencensus.trace.TraceOptions;
+import java.util.Collection;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A service that is used by the library to export {@code SpanData} for all the spans that are part
+ * of a distributed sampled trace (see {@link TraceOptions#isSampled()}).
+ *
+ * @since 0.5
+ */
+@ThreadSafe
+public abstract class SpanExporter {
+ private static final SpanExporter NOOP_SPAN_EXPORTER = new NoopSpanExporter();
+
+ /**
+ * Returns the no-op implementation of the {@code ExportComponent}.
+ *
+ * @return the no-op implementation of the {@code ExportComponent}.
+ * @since 0.5
+ */
+ public static SpanExporter getNoopSpanExporter() {
+ return NOOP_SPAN_EXPORTER;
+ }
+
+ /**
+ * Registers a new service handler that is used by the library to export {@code SpanData} for
+ * sampled spans (see {@link TraceOptions#isSampled()}).
+ *
+ * @param name the name of the service handler. Must be unique for each service.
+ * @param handler the service handler that is called for each ended sampled span.
+ * @since 0.5
+ */
+ public abstract void registerHandler(String name, Handler handler);
+
+ /**
+ * Unregisters the service handler with the provided name.
+ *
+ * @param name the name of the service handler that will be unregistered.
+ * @since 0.5
+ */
+ public abstract void unregisterHandler(String name);
+
+ /**
+ * An abstract class that allows different tracing services to export recorded data for sampled
+ * spans in their own format.
+ *
+ * <p>To export data this MUST be register to to the ExportComponent using {@link
+ * #registerHandler(String, Handler)}.
+ *
+ * @since 0.5
+ */
+ public abstract static class Handler {
+
+ /**
+ * Exports a list of sampled (see {@link TraceOptions#isSampled()}) {@link Span}s using the
+ * immutable representation {@link SpanData}.
+ *
+ * <p>This may be called from a different thread than the one that called {@link Span#end()}.
+ *
+ * <p>Implementation SHOULD not block the calling thread. It should execute the export on a
+ * different thread if possible.
+ *
+ * @param spanDataList a list of {@code SpanData} objects to be exported.
+ * @since 0.5
+ */
+ public abstract void export(Collection<SpanData> spanDataList);
+ }
+
+ private static final class NoopSpanExporter extends SpanExporter {
+
+ @Override
+ public void registerHandler(String name, Handler handler) {}
+
+ @Override
+ public void unregisterHandler(String name) {}
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/internal/BaseMessageEventUtils.java b/api/src/main/java/io/opencensus/trace/internal/BaseMessageEventUtils.java
new file mode 100644
index 00000000..9d22a1c6
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/internal/BaseMessageEventUtils.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018, 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.trace.internal;
+
+import io.opencensus.common.Internal;
+import io.opencensus.internal.Utils;
+
+/**
+ * Helper class to convert/cast between for {@link io.opencensus.trace.MessageEvent} and {@link
+ * io.opencensus.trace.NetworkEvent}.
+ */
+@Internal
+@SuppressWarnings("deprecation")
+public final class BaseMessageEventUtils {
+ /**
+ * Cast or convert a {@link io.opencensus.trace.BaseMessageEvent} to {@link
+ * io.opencensus.trace.MessageEvent}.
+ *
+ * <p>Warning: if the input is a {@code io.opencensus.trace.NetworkEvent} and contains {@code
+ * kernelTimestamp} information, this information will be dropped.
+ *
+ * @param event the {@code BaseMessageEvent} that is being cast or converted.
+ * @return a {@code MessageEvent} representation of the input.
+ */
+ public static io.opencensus.trace.MessageEvent asMessageEvent(
+ io.opencensus.trace.BaseMessageEvent event) {
+ Utils.checkNotNull(event, "event");
+ if (event instanceof io.opencensus.trace.MessageEvent) {
+ return (io.opencensus.trace.MessageEvent) event;
+ }
+ io.opencensus.trace.NetworkEvent networkEvent = (io.opencensus.trace.NetworkEvent) event;
+ io.opencensus.trace.MessageEvent.Type type =
+ (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.RECV)
+ ? io.opencensus.trace.MessageEvent.Type.RECEIVED
+ : io.opencensus.trace.MessageEvent.Type.SENT;
+ return io.opencensus.trace.MessageEvent.builder(type, networkEvent.getMessageId())
+ .setUncompressedMessageSize(networkEvent.getUncompressedMessageSize())
+ .setCompressedMessageSize(networkEvent.getCompressedMessageSize())
+ .build();
+ }
+
+ /**
+ * Cast or convert a {@link io.opencensus.trace.BaseMessageEvent} to {@link
+ * io.opencensus.trace.NetworkEvent}.
+ *
+ * @param event the {@code BaseMessageEvent} that is being cast or converted.
+ * @return a {@code io.opencensus.trace.NetworkEvent} representation of the input.
+ */
+ public static io.opencensus.trace.NetworkEvent asNetworkEvent(
+ io.opencensus.trace.BaseMessageEvent event) {
+ Utils.checkNotNull(event, "event");
+ if (event instanceof io.opencensus.trace.NetworkEvent) {
+ return (io.opencensus.trace.NetworkEvent) event;
+ }
+ io.opencensus.trace.MessageEvent messageEvent = (io.opencensus.trace.MessageEvent) event;
+ io.opencensus.trace.NetworkEvent.Type type =
+ (messageEvent.getType() == io.opencensus.trace.MessageEvent.Type.RECEIVED)
+ ? io.opencensus.trace.NetworkEvent.Type.RECV
+ : io.opencensus.trace.NetworkEvent.Type.SENT;
+ return io.opencensus.trace.NetworkEvent.builder(type, messageEvent.getMessageId())
+ .setUncompressedMessageSize(messageEvent.getUncompressedMessageSize())
+ .setCompressedMessageSize(messageEvent.getCompressedMessageSize())
+ .build();
+ }
+
+ private BaseMessageEventUtils() {}
+}
diff --git a/api/src/main/java/io/opencensus/trace/package-info.java b/api/src/main/java/io/opencensus/trace/package-info.java
new file mode 100644
index 00000000..77f39aba
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/package-info.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/**
+ * API for distributed tracing.
+ *
+ * <p>Distributed tracing, also called distributed request tracing, is a technique that helps
+ * debugging distributed applications.
+ *
+ * <p>Trace represents a tree of spans. A trace has a root span that encapsulates all the spans from
+ * start to end, and the children spans being the distinct calls invoked in between.
+ *
+ * <p>{@link io.opencensus.trace.Span} represents a single operation within a trace.
+ *
+ * <p>{@link io.opencensus.trace.Span Spans} are propagated in-process in the {@code
+ * io.grpc.Context} and between process using one of the wire propagation formats supported in the
+ * {@code io.opencensus.trace.propagation} package.
+ */
+// TODO: Add code examples.
+package io.opencensus.trace;
diff --git a/api/src/main/java/io/opencensus/trace/propagation/BinaryFormat.java b/api/src/main/java/io/opencensus/trace/propagation/BinaryFormat.java
new file mode 100644
index 00000000..7e875fd6
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/propagation/BinaryFormat.java
@@ -0,0 +1,156 @@
+/*
+ * 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.trace.propagation;
+
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.SpanContext;
+import java.text.ParseException;
+
+/**
+ * This is a helper class for {@link SpanContext} propagation on the wire using binary encoding.
+ *
+ * <p>Example of usage on the client:
+ *
+ * <pre>{@code
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private static final BinaryFormat binaryFormat =
+ * Tracing.getPropagationComponent().getBinaryFormat();
+ * void onSendRequest() {
+ * try (Scope ss = tracer.spanBuilder("Sent.MyRequest").startScopedSpan()) {
+ * byte[] binaryValue = binaryFormat.toByteArray(tracer.getCurrentContext().context());
+ * // Send the request including the binaryValue and wait for the response.
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Example of usage on the server:
+ *
+ * <pre>{@code
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private static final BinaryFormat binaryFormat =
+ * Tracing.getPropagationComponent().getBinaryFormat();
+ * void onRequestReceived() {
+ * // Get the binaryValue from the request.
+ * SpanContext spanContext = SpanContext.INVALID;
+ * try {
+ * if (binaryValue != null) {
+ * spanContext = binaryFormat.fromByteArray(binaryValue);
+ * }
+ * } catch (SpanContextParseException e) {
+ * // Maybe log the exception.
+ * }
+ * try (Scope ss =
+ * tracer.spanBuilderWithRemoteParent("Recv.MyRequest", spanContext).startScopedSpan()) {
+ * // Handle request and send response back.
+ * }
+ * }
+ * }</pre>
+ *
+ * @since 0.5
+ */
+public abstract class BinaryFormat {
+ static final NoopBinaryFormat NOOP_BINARY_FORMAT = new NoopBinaryFormat();
+
+ /**
+ * Serializes a {@link SpanContext} into a byte array using the binary format.
+ *
+ * @deprecated use {@link #toByteArray(SpanContext)}.
+ * @param spanContext the {@code SpanContext} to serialize.
+ * @return the serialized binary value.
+ * @throws NullPointerException if the {@code spanContext} is {@code null}.
+ * @since 0.5
+ */
+ @Deprecated
+ public byte[] toBinaryValue(SpanContext spanContext) {
+ return toByteArray(spanContext);
+ }
+
+ /**
+ * Serializes a {@link SpanContext} into a byte array using the binary format.
+ *
+ * @param spanContext the {@code SpanContext} to serialize.
+ * @return the serialized binary value.
+ * @throws NullPointerException if the {@code spanContext} is {@code null}.
+ * @since 0.7
+ */
+ public byte[] toByteArray(SpanContext spanContext) {
+ // Implementation must override this method.
+ return toBinaryValue(spanContext);
+ }
+
+ /**
+ * Parses the {@link SpanContext} from a byte array using the binary format.
+ *
+ * @deprecated use {@link #fromByteArray(byte[])}.
+ * @param bytes a binary encoded buffer from which the {@code SpanContext} will be parsed.
+ * @return the parsed {@code SpanContext}.
+ * @throws NullPointerException if the {@code input} is {@code null}.
+ * @throws ParseException if the version is not supported or the input is invalid
+ * @since 0.5
+ */
+ @Deprecated
+ public SpanContext fromBinaryValue(byte[] bytes) throws ParseException {
+ try {
+ return fromByteArray(bytes);
+ } catch (SpanContextParseException e) {
+ throw new ParseException(e.toString(), 0);
+ }
+ }
+
+ /**
+ * Parses the {@link SpanContext} from a byte array using the binary format.
+ *
+ * @param bytes a binary encoded buffer from which the {@code SpanContext} will be parsed.
+ * @return the parsed {@code SpanContext}.
+ * @throws NullPointerException if the {@code input} is {@code null}.
+ * @throws SpanContextParseException if the version is not supported or the input is invalid
+ * @since 0.7
+ */
+ public SpanContext fromByteArray(byte[] bytes) throws SpanContextParseException {
+ // Implementation must override this method. If it doesn't, the below will StackOverflowError.
+ try {
+ return fromBinaryValue(bytes);
+ } catch (ParseException e) {
+ throw new SpanContextParseException("Error while parsing.", e);
+ }
+ }
+
+ /**
+ * Returns the no-op implementation of the {@code BinaryFormat}.
+ *
+ * @return the no-op implementation of the {@code BinaryFormat}.
+ */
+ static BinaryFormat getNoopBinaryFormat() {
+ return NOOP_BINARY_FORMAT;
+ }
+
+ private static final class NoopBinaryFormat extends BinaryFormat {
+ @Override
+ public byte[] toByteArray(SpanContext spanContext) {
+ Utils.checkNotNull(spanContext, "spanContext");
+ return new byte[0];
+ }
+
+ @Override
+ public SpanContext fromByteArray(byte[] bytes) {
+ Utils.checkNotNull(bytes, "bytes");
+ return SpanContext.INVALID;
+ }
+
+ private NoopBinaryFormat() {}
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/propagation/PropagationComponent.java b/api/src/main/java/io/opencensus/trace/propagation/PropagationComponent.java
new file mode 100644
index 00000000..a90f0419
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/propagation/PropagationComponent.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.trace.propagation;
+
+import io.opencensus.common.ExperimentalApi;
+
+/**
+ * Container class for all the supported propagation formats. Currently supports only Binary format
+ * (see {@link BinaryFormat}) and B3 Text format (see {@link TextFormat}) but more formats will be
+ * added.
+ *
+ * @since 0.5
+ */
+public abstract class PropagationComponent {
+ private static final PropagationComponent NOOP_PROPAGATION_COMPONENT =
+ new NoopPropagationComponent();
+
+ /**
+ * Returns the {@link BinaryFormat} with the provided implementations. If no implementation is
+ * provided then no-op implementation will be used.
+ *
+ * @return the {@code BinaryFormat} implementation.
+ * @since 0.5
+ */
+ public abstract BinaryFormat getBinaryFormat();
+
+ /**
+ * Returns the B3 {@link TextFormat} with the provided implementations. See <a
+ * href="https://github.com/openzipkin/b3-propagation">b3-propagation</a> for more information. If
+ * no implementation is provided then no-op implementation will be used.
+ *
+ * @since 0.11.0
+ * @return the B3 {@code TextFormat} implementation for B3.
+ */
+ @ExperimentalApi
+ public abstract TextFormat getB3Format();
+
+ /**
+ * Returns an instance that contains no-op implementations for all the instances.
+ *
+ * @return an instance that contains no-op implementations for all the instances.
+ * @since 0.5
+ */
+ public static PropagationComponent getNoopPropagationComponent() {
+ return NOOP_PROPAGATION_COMPONENT;
+ }
+
+ private static final class NoopPropagationComponent extends PropagationComponent {
+ @Override
+ public BinaryFormat getBinaryFormat() {
+ return BinaryFormat.getNoopBinaryFormat();
+ }
+
+ @Override
+ public TextFormat getB3Format() {
+ return TextFormat.getNoopTextFormat();
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/propagation/SpanContextParseException.java b/api/src/main/java/io/opencensus/trace/propagation/SpanContextParseException.java
new file mode 100644
index 00000000..80d42af5
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/propagation/SpanContextParseException.java
@@ -0,0 +1,47 @@
+/*
+ * 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.trace.propagation;
+
+/**
+ * Exception thrown when a {@link io.opencensus.trace.SpanContext} cannot be parsed.
+ *
+ * @since 0.7
+ */
+public final class SpanContextParseException extends Exception {
+ private static final long serialVersionUID = 0L;
+
+ /**
+ * Constructs a new {@code SpanContextParseException} with the given message.
+ *
+ * @param message a message describing the parse error.
+ * @since 0.7
+ */
+ public SpanContextParseException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@code SpanContextParseException} with the given message and cause.
+ *
+ * @param message a message describing the parse error.
+ * @param cause the cause of the parse error.
+ * @since 0.7
+ */
+ public SpanContextParseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/propagation/TextFormat.java b/api/src/main/java/io/opencensus/trace/propagation/TextFormat.java
new file mode 100644
index 00000000..d52e71f1
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/propagation/TextFormat.java
@@ -0,0 +1,204 @@
+/*
+ * 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.trace.propagation;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.SpanContext;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.NonNull;
+*/
+
+/**
+ * Injects and extracts {@link SpanContext trace identifiers} as text into carriers that travel
+ * in-band across process boundaries. Identifiers are often encoded as messaging or RPC request
+ * headers.
+ *
+ * <p>When using http, the carrier of propagated data on both the client (injector) and server
+ * (extractor) side is usually an http request. Propagation is usually implemented via library-
+ * specific request interceptors, where the client-side injects span identifiers and the server-side
+ * extracts them.
+ *
+ * <p>Example of usage on the client:
+ *
+ * <pre>{@code
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private static final TextFormat textFormat = Tracing.getPropagationComponent().getTextFormat();
+ * private static final TextFormat.Setter setter = new TextFormat.Setter<HttpURLConnection>() {
+ * public void put(HttpURLConnection carrier, String key, String value) {
+ * carrier.setRequestProperty(field, value);
+ * }
+ * }
+ *
+ * void makeHttpRequest() {
+ * Span span = tracer.spanBuilder("Sent.MyRequest").startSpan();
+ * try (Scope s = tracer.withSpan(span)) {
+ * HttpURLConnection connection =
+ * (HttpURLConnection) new URL("http://myserver").openConnection();
+ * textFormat.inject(span.getContext(), connection, httpURLConnectionSetter);
+ * // Send the request, wait for response and maybe set the status if not ok.
+ * }
+ * span.end(); // Can set a status.
+ * }
+ * }</pre>
+ *
+ * <p>Example of usage on the server:
+ *
+ * <pre>{@code
+ * private static final Tracer tracer = Tracing.getTracer();
+ * private static final TextFormat textFormat = Tracing.getPropagationComponent().getTextFormat();
+ * private static final TextFormat.Getter<HttpRequest> getter = ...;
+ *
+ * void onRequestReceived(HttpRequest request) {
+ * SpanContext spanContext = textFormat.extract(request, getter);
+ * Span span = tracer.spanBuilderWithRemoteParent("Recv.MyRequest", spanContext).startSpan();
+ * try (Scope s = tracer.withSpan(span)) {
+ * // Handle request and send response back.
+ * }
+ * span.end()
+ * }
+ * }</pre>
+ *
+ * @since 0.11
+ */
+@ExperimentalApi
+public abstract class TextFormat {
+ private static final NoopTextFormat NOOP_TEXT_FORMAT = new NoopTextFormat();
+
+ /**
+ * The propagation fields defined. If your carrier is reused, you should delete the fields here
+ * before calling {@link #inject(SpanContext, Object, Setter)}.
+ *
+ * <p>For example, if the carrier is a single-use or immutable request object, you don't need to
+ * clear fields as they couldn't have been set before. If it is a mutable, retryable object,
+ * successive calls should clear these fields first.
+ *
+ * @since 0.11
+ */
+ // The use cases of this are:
+ // * allow pre-allocation of fields, especially in systems like gRPC Metadata
+ // * allow a single-pass over an iterator (ex OpenTracing has no getter in TextMap)
+ public abstract List<String> fields();
+
+ /**
+ * Injects the span context downstream. For example, as http headers.
+ *
+ * @param spanContext possibly not sampled.
+ * @param carrier holds propagation fields. For example, an outgoing message or http request.
+ * @param setter invoked for each propagation key to add or remove.
+ * @since 0.11
+ */
+ public abstract <C /*>>> extends @NonNull Object*/> void inject(
+ SpanContext spanContext, C carrier, Setter<C> setter);
+
+ /**
+ * Class that allows a {@code TextFormat} to set propagated fields into a carrier.
+ *
+ * <p>{@code Setter} is stateless and allows to be saved as a constant to avoid runtime
+ * allocations.
+ *
+ * @param <C> carrier of propagation fields, such as an http request
+ * @since 0.11
+ */
+ public abstract static class Setter<C> {
+
+ /**
+ * Replaces a propagated field with the given value.
+ *
+ * <p>For example, a setter for an {@link java.net.HttpURLConnection} would be the method
+ * reference {@link java.net.HttpURLConnection#addRequestProperty(String, String)}
+ *
+ * @param carrier holds propagation fields. For example, an outgoing message or http request.
+ * @param key the key of the field.
+ * @param value the value of the field.
+ * @since 0.11
+ */
+ public abstract void put(C carrier, String key, String value);
+ }
+
+ /**
+ * Extracts the span context from upstream. For example, as http headers.
+ *
+ * @param carrier holds propagation fields. For example, an outgoing message or http request.
+ * @param getter invoked for each propagation key to get.
+ * @throws SpanContextParseException if the input is invalid
+ * @since 0.11
+ */
+ public abstract <C /*>>> extends @NonNull Object*/> SpanContext extract(
+ C carrier, Getter<C> getter) throws SpanContextParseException;
+
+ /**
+ * Class that allows a {@code TextFormat} to read propagated fields from a carrier.
+ *
+ * <p>{@code Getter} is stateless and allows to be saved as a constant to avoid runtime
+ * allocations.
+ *
+ * @param <C> carrier of propagation fields, such as an http request
+ * @since 0.11
+ */
+ public abstract static class Getter<C> {
+
+ /**
+ * Returns the first value of the given propagation {@code key} or returns {@code null}.
+ *
+ * @param carrier carrier of propagation fields, such as an http request
+ * @param key the key of the field.
+ * @return the first value of the given propagation {@code key} or returns {@code null}.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract String get(C carrier, String key);
+ }
+
+ /**
+ * Returns the no-op implementation of the {@code TextFormat}.
+ *
+ * @return the no-op implementation of the {@code TextFormat}.
+ */
+ static TextFormat getNoopTextFormat() {
+ return NOOP_TEXT_FORMAT;
+ }
+
+ private static final class NoopTextFormat extends TextFormat {
+
+ private NoopTextFormat() {}
+
+ @Override
+ public List<String> fields() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public <C /*>>> extends @NonNull Object*/> void inject(
+ SpanContext spanContext, C carrier, Setter<C> setter) {
+ Utils.checkNotNull(spanContext, "spanContext");
+ Utils.checkNotNull(carrier, "carrier");
+ Utils.checkNotNull(setter, "setter");
+ }
+
+ @Override
+ public <C /*>>> extends @NonNull Object*/> SpanContext extract(C carrier, Getter<C> getter) {
+ Utils.checkNotNull(carrier, "carrier");
+ Utils.checkNotNull(getter, "getter");
+ return SpanContext.INVALID;
+ }
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/samplers/AlwaysSampleSampler.java b/api/src/main/java/io/opencensus/trace/samplers/AlwaysSampleSampler.java
new file mode 100644
index 00000000..7b61e235
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/samplers/AlwaysSampleSampler.java
@@ -0,0 +1,55 @@
+/*
+ * 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.trace.samplers;
+
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/** Sampler that always makes a "yes" decision on {@link Span} sampling. */
+@Immutable
+final class AlwaysSampleSampler extends Sampler {
+
+ AlwaysSampleSampler() {}
+
+ // Returns always makes a "yes" decision on {@link Span} sampling.
+ @Override
+ public boolean shouldSample(
+ @Nullable SpanContext parentContext,
+ @Nullable Boolean hasRemoteParent,
+ TraceId traceId,
+ SpanId spanId,
+ String name,
+ List<Span> parentLinks) {
+ return true;
+ }
+
+ @Override
+ public String getDescription() {
+ return toString();
+ }
+
+ @Override
+ public String toString() {
+ return "AlwaysSampleSampler";
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/samplers/NeverSampleSampler.java b/api/src/main/java/io/opencensus/trace/samplers/NeverSampleSampler.java
new file mode 100644
index 00000000..c6de645a
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/samplers/NeverSampleSampler.java
@@ -0,0 +1,55 @@
+/*
+ * 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.trace.samplers;
+
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/** Sampler that always makes a "no" decision on {@link Span} sampling. */
+@Immutable
+final class NeverSampleSampler extends Sampler {
+
+ NeverSampleSampler() {}
+
+ // Returns always makes a "no" decision on {@link Span} sampling.
+ @Override
+ public boolean shouldSample(
+ @Nullable SpanContext parentContext,
+ @Nullable Boolean hasRemoteParent,
+ TraceId traceId,
+ SpanId spanId,
+ String name,
+ List<Span> parentLinks) {
+ return false;
+ }
+
+ @Override
+ public String getDescription() {
+ return toString();
+ }
+
+ @Override
+ public String toString() {
+ return "NeverSampleSampler";
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/samplers/ProbabilitySampler.java b/api/src/main/java/io/opencensus/trace/samplers/ProbabilitySampler.java
new file mode 100644
index 00000000..b9c18e00
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/samplers/ProbabilitySampler.java
@@ -0,0 +1,107 @@
+/*
+ * 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.trace.samplers;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.internal.Utils;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * We assume the lower 64 bits of the traceId's are randomly distributed around the whole (long)
+ * range. We convert an incoming probability into an upper bound on that value, such that we can
+ * just compare the absolute value of the id and the bound to see if we are within the desired
+ * probability range. Using the low bits of the traceId also ensures that systems that only use 64
+ * bit ID's will also work with this sampler.
+ */
+@AutoValue
+@Immutable
+abstract class ProbabilitySampler extends Sampler {
+
+ ProbabilitySampler() {}
+
+ abstract double getProbability();
+
+ abstract long getIdUpperBound();
+
+ /**
+ * Returns a new {@link ProbabilitySampler}. The probability of sampling a trace is equal to that
+ * of the specified probability.
+ *
+ * @param probability The desired probability of sampling. Must be within [0.0, 1.0].
+ * @return a new {@link ProbabilitySampler}.
+ * @throws IllegalArgumentException if {@code probability} is out of range
+ */
+ static ProbabilitySampler create(double probability) {
+ Utils.checkArgument(
+ probability >= 0.0 && probability <= 1.0, "probability must be in range [0.0, 1.0]");
+ long idUpperBound;
+ // Special case the limits, to avoid any possible issues with lack of precision across
+ // double/long boundaries. For probability == 0.0, we use Long.MIN_VALUE as this guarantees
+ // that we will never sample a trace, even in the case where the id == Long.MIN_VALUE, since
+ // Math.Abs(Long.MIN_VALUE) == Long.MIN_VALUE.
+ if (probability == 0.0) {
+ idUpperBound = Long.MIN_VALUE;
+ } else if (probability == 1.0) {
+ idUpperBound = Long.MAX_VALUE;
+ } else {
+ idUpperBound = (long) (probability * Long.MAX_VALUE);
+ }
+ return new AutoValue_ProbabilitySampler(probability, idUpperBound);
+ }
+
+ @Override
+ public final boolean shouldSample(
+ @Nullable SpanContext parentContext,
+ @Nullable Boolean hasRemoteParent,
+ TraceId traceId,
+ SpanId spanId,
+ String name,
+ @Nullable List<Span> parentLinks) {
+ // If the parent is sampled keep the sampling decision.
+ if (parentContext != null && parentContext.getTraceOptions().isSampled()) {
+ return true;
+ }
+ if (parentLinks != null) {
+ // If any parent link is sampled keep the sampling decision.
+ for (Span parentLink : parentLinks) {
+ if (parentLink.getContext().getTraceOptions().isSampled()) {
+ return true;
+ }
+ }
+ }
+ // Always sample if we are within probability range. This is true even for child spans (that
+ // may have had a different sampling decision made) to allow for different sampling policies,
+ // and dynamic increases to sampling probabilities for debugging purposes.
+ // Note use of '<' for comparison. This ensures that we never sample for probability == 0.0,
+ // while allowing for a (very) small chance of *not* sampling if the id == Long.MAX_VALUE.
+ // This is considered a reasonable tradeoff for the simplicity/performance requirements (this
+ // code is executed in-line for every Span creation).
+ return Math.abs(traceId.getLowerLong()) < getIdUpperBound();
+ }
+
+ @Override
+ public final String getDescription() {
+ return String.format("ProbabilitySampler{%.6f}", getProbability());
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/samplers/Samplers.java b/api/src/main/java/io/opencensus/trace/samplers/Samplers.java
new file mode 100644
index 00000000..c10610a0
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/samplers/Samplers.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.trace.samplers;
+
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+
+/**
+ * Static class to access a set of pre-defined {@link Sampler Samplers}.
+ *
+ * @since 0.5
+ */
+public final class Samplers {
+ private static final Sampler ALWAYS_SAMPLE = new AlwaysSampleSampler();
+ private static final Sampler NEVER_SAMPLE = new NeverSampleSampler();
+
+ // No instance of this class.
+ private Samplers() {}
+
+ /**
+ * Returns a {@link Sampler} that always makes a "yes" decision on {@link Span} sampling.
+ *
+ * @return a {@code Sampler} that always makes a "yes" decision on {@code Span} sampling.
+ * @since 0.5
+ */
+ public static Sampler alwaysSample() {
+ return ALWAYS_SAMPLE;
+ }
+
+ /**
+ * Returns a {@link Sampler} that always makes a "no" decision on {@link Span} sampling.
+ *
+ * @return a {@code Sampler} that always makes a "no" decision on {@code Span} sampling.
+ * @since 0.5
+ */
+ public static Sampler neverSample() {
+ return NEVER_SAMPLE;
+ }
+
+ /**
+ * Returns a {@link Sampler} that makes a "yes" decision with a given probability.
+ *
+ * @param probability The desired probability of sampling. Must be within [0.0, 1.0].
+ * @return a {@code Sampler} that makes a "yes" decision with a given probability.
+ * @throws IllegalArgumentException if {@code probability} is out of range
+ * @since 0.5
+ */
+ public static Sampler probabilitySampler(double probability) {
+ return ProbabilitySampler.create(probability);
+ }
+}
diff --git a/api/src/main/java/io/opencensus/trace/unsafe/ContextUtils.java b/api/src/main/java/io/opencensus/trace/unsafe/ContextUtils.java
new file mode 100644
index 00000000..3f4b9889
--- /dev/null
+++ b/api/src/main/java/io/opencensus/trace/unsafe/ContextUtils.java
@@ -0,0 +1,45 @@
+/*
+ * 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.trace.unsafe;
+
+import io.grpc.Context;
+import io.opencensus.trace.Span;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Util methods/functionality to interact with the {@link io.grpc.Context}.
+ *
+ * <p>Users must interact with the current Context via the public APIs in {@link
+ * io.opencensus.trace.Tracer} and avoid usages of the {@link #CONTEXT_SPAN_KEY} directly.
+ *
+ * @since 0.5
+ */
+public final class ContextUtils {
+ // No instance of this class.
+ private ContextUtils() {}
+
+ /**
+ * The {@link io.grpc.Context.Key} used to interact with {@link io.grpc.Context}.
+ *
+ * @since 0.5
+ */
+ public static final Context.Key</*@Nullable*/ Span> CONTEXT_SPAN_KEY =
+ Context.key("opencensus-trace-span-key");
+}
diff --git a/api/src/test/java/io/opencensus/common/DurationTest.java b/api/src/test/java/io/opencensus/common/DurationTest.java
new file mode 100644
index 00000000..ea636ca0
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/DurationTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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 Duration}. */
+@RunWith(JUnit4.class)
+public class DurationTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testDurationCreate() {
+ assertThat(Duration.create(24, 42).getSeconds()).isEqualTo(24);
+ assertThat(Duration.create(24, 42).getNanos()).isEqualTo(42);
+ assertThat(Duration.create(-24, -42).getSeconds()).isEqualTo(-24);
+ assertThat(Duration.create(-24, -42).getNanos()).isEqualTo(-42);
+ assertThat(Duration.create(315576000000L, 999999999).getSeconds()).isEqualTo(315576000000L);
+ assertThat(Duration.create(315576000000L, 999999999).getNanos()).isEqualTo(999999999);
+ assertThat(Duration.create(-315576000000L, -999999999).getSeconds()).isEqualTo(-315576000000L);
+ assertThat(Duration.create(-315576000000L, -999999999).getNanos()).isEqualTo(-999999999);
+ }
+
+ @Test
+ public void create_SecondsTooLow() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' is less than minimum (-315576000000): -315576000001");
+ Duration.create(-315576000001L, 0);
+ }
+
+ @Test
+ public void create_SecondsTooHigh() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' is greater than maximum (315576000000): 315576000001");
+ Duration.create(315576000001L, 0);
+ }
+
+ @Test
+ public void create_NanosTooLow() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'nanos' is less than minimum (-999999999): -1000000000");
+ Duration.create(0, -1000000000);
+ }
+
+ @Test
+ public void create_NanosTooHigh() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'nanos' is greater than maximum (999999999): 1000000000");
+ Duration.create(0, 1000000000);
+ }
+
+ @Test
+ public void create_NegativeSecondsPositiveNanos() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' and 'nanos' have inconsistent sign: seconds=-1, nanos=1");
+ Duration.create(-1, 1);
+ }
+
+ @Test
+ public void create_PositiveSecondsNegativeNanos() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' and 'nanos' have inconsistent sign: seconds=1, nanos=-1");
+ Duration.create(1, -1);
+ }
+
+ @Test
+ public void testDurationFromMillis() {
+ assertThat(Duration.fromMillis(0)).isEqualTo(Duration.create(0, 0));
+ assertThat(Duration.fromMillis(987)).isEqualTo(Duration.create(0, 987000000));
+ assertThat(Duration.fromMillis(3456)).isEqualTo(Duration.create(3, 456000000));
+ }
+
+ @Test
+ public void testDurationFromMillisNegative() {
+ assertThat(Duration.fromMillis(-1)).isEqualTo(Duration.create(0, -1000000));
+ assertThat(Duration.fromMillis(-999)).isEqualTo(Duration.create(0, -999000000));
+ assertThat(Duration.fromMillis(-1000)).isEqualTo(Duration.create(-1, 0));
+ assertThat(Duration.fromMillis(-3456)).isEqualTo(Duration.create(-3, -456000000));
+ }
+
+ @Test
+ public void fromMillis_TooLow() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' is less than minimum (-315576000000): -315576000001");
+ Duration.fromMillis(-315576000001000L);
+ }
+
+ @Test
+ public void fromMillis_TooHigh() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' is greater than maximum (315576000000): 315576000001");
+ Duration.fromMillis(315576000001000L);
+ }
+
+ @Test
+ public void duration_CompareLength() {
+ assertThat(Duration.create(0, 0).compareTo(Duration.create(0, 0))).isEqualTo(0);
+ assertThat(Duration.create(24, 42).compareTo(Duration.create(24, 42))).isEqualTo(0);
+ assertThat(Duration.create(-24, -42).compareTo(Duration.create(-24, -42))).isEqualTo(0);
+ assertThat(Duration.create(25, 42).compareTo(Duration.create(24, 42))).isEqualTo(1);
+ assertThat(Duration.create(24, 45).compareTo(Duration.create(24, 42))).isEqualTo(1);
+ assertThat(Duration.create(24, 42).compareTo(Duration.create(25, 42))).isEqualTo(-1);
+ assertThat(Duration.create(24, 42).compareTo(Duration.create(24, 45))).isEqualTo(-1);
+ assertThat(Duration.create(-24, -45).compareTo(Duration.create(-24, -42))).isEqualTo(-1);
+ assertThat(Duration.create(-24, -42).compareTo(Duration.create(-25, -42))).isEqualTo(1);
+ assertThat(Duration.create(24, 42).compareTo(Duration.create(-24, -42))).isEqualTo(1);
+ }
+
+ @Test
+ public void testDurationEqual() {
+ // Positive tests.
+ assertThat(Duration.create(0, 0)).isEqualTo(Duration.create(0, 0));
+ assertThat(Duration.create(24, 42)).isEqualTo(Duration.create(24, 42));
+ assertThat(Duration.create(-24, -42)).isEqualTo(Duration.create(-24, -42));
+ // Negative tests.
+ assertThat(Duration.create(25, 42)).isNotEqualTo(Duration.create(24, 42));
+ assertThat(Duration.create(24, 43)).isNotEqualTo(Duration.create(24, 42));
+ assertThat(Duration.create(-25, -42)).isNotEqualTo(Duration.create(-24, -42));
+ assertThat(Duration.create(-24, -43)).isNotEqualTo(Duration.create(-24, -42));
+ }
+
+ @Test
+ public void toMillis() {
+ assertThat(Duration.create(10, 0).toMillis()).isEqualTo(10000L);
+ assertThat(Duration.create(10, 1000).toMillis()).isEqualTo(10000L);
+ assertThat(Duration.create(0, (int) 1e6).toMillis()).isEqualTo(1L);
+ assertThat(Duration.create(0, 0).toMillis()).isEqualTo(0L);
+ assertThat(Duration.create(-10, 0).toMillis()).isEqualTo(-10000L);
+ assertThat(Duration.create(-10, -1000).toMillis()).isEqualTo(-10000L);
+ assertThat(Duration.create(0, -(int) 1e6).toMillis()).isEqualTo(-1L);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/common/FunctionsTest.java b/api/src/test/java/io/opencensus/common/FunctionsTest.java
new file mode 100644
index 00000000..55d58d4d
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/FunctionsTest.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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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 Functions}. */
+@RunWith(JUnit4.class)
+public class FunctionsTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testReturnNull() {
+ assertThat(Functions.returnNull().apply("ignored")).isNull();
+ }
+
+ @Test
+ public void testReturnConstant() {
+ assertThat(Functions.returnConstant(123).apply("ignored")).isEqualTo(123);
+ }
+
+ @Test
+ public void testReturnToString() {
+ assertThat(Functions.returnToString().apply("input")).isEqualTo("input");
+ assertThat(Functions.returnToString().apply(Boolean.FALSE)).isEqualTo("false");
+ assertThat(Functions.returnToString().apply(Double.valueOf(123.45))).isEqualTo("123.45");
+ assertThat(Functions.returnToString().apply(null)).isEqualTo(null);
+ }
+
+ @Test
+ public void testThrowIllegalArgumentException() {
+ Function<Object, Void> f = Functions.throwIllegalArgumentException();
+ thrown.expect(IllegalArgumentException.class);
+ f.apply("ignored");
+ }
+
+ @Test
+ public void testThrowAssertionError() {
+ Function<Object, Void> f = Functions.throwAssertionError();
+ thrown.handleAssertionErrors();
+ thrown.expect(AssertionError.class);
+ f.apply("ignored");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/common/ServerStatsEncodingTest.java b/api/src/test/java/io/opencensus/common/ServerStatsEncodingTest.java
new file mode 100644
index 00000000..6db14a79
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/ServerStatsEncodingTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018, 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+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 ServerStatsEncoding}. */
+@RunWith(JUnit4.class)
+public class ServerStatsEncodingTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void encodeDecodeTest() throws ServerStatsDeserializationException {
+ ServerStats serverStatsToBeEncoded = null;
+ ServerStats serverStatsDecoded = null;
+ byte[] serialized = null;
+
+ serverStatsToBeEncoded = ServerStats.create(31, 22, (byte) 1);
+ serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+ serverStatsDecoded = ServerStatsEncoding.parseBytes(serialized);
+ assertThat(serverStatsDecoded).isEqualTo(serverStatsToBeEncoded);
+
+ serverStatsToBeEncoded = ServerStats.create(0, 22, (byte) 0);
+ serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+ serverStatsDecoded = ServerStatsEncoding.parseBytes(serialized);
+ assertThat(serverStatsDecoded).isEqualTo(serverStatsToBeEncoded);
+
+ serverStatsToBeEncoded = ServerStats.create(450, 0, (byte) 0);
+ serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+ serverStatsDecoded = ServerStatsEncoding.parseBytes(serialized);
+ assertThat(serverStatsDecoded).isEqualTo(serverStatsToBeEncoded);
+ }
+
+ @Test
+ public void skipUnknownFieldTest() throws ServerStatsDeserializationException {
+ ServerStats serverStatsToBeEncoded = null;
+ ServerStats serverStatsDecoded = null;
+ byte[] serialized = null;
+
+ serverStatsToBeEncoded = ServerStats.create(31, 22, (byte) 1);
+ serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+
+ // Add new field at the end.
+ byte[] serializedExpanded = new byte[serialized.length + 9];
+ System.arraycopy(serialized, 0, serializedExpanded, 0, serialized.length);
+ final ByteBuffer bb = ByteBuffer.wrap(serializedExpanded);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ bb.position(serialized.length);
+ bb.put((byte) 255);
+ bb.putLong(0L);
+ byte[] newSerialized = bb.array();
+
+ serverStatsDecoded = ServerStatsEncoding.parseBytes(newSerialized);
+ assertThat(serverStatsDecoded).isEqualTo(serverStatsToBeEncoded);
+ }
+
+ @Test
+ public void negativeLbLatencyValueTest() throws ServerStatsDeserializationException {
+ ServerStats serverStatsToBeEncoded = null;
+ ServerStats serverStatsDecoded = null;
+ byte[] serialized = null;
+
+ serverStatsToBeEncoded = ServerStats.create(31, 22, (byte) 1);
+ serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+
+ // update serialized byte[] with negative value for lbLatency.
+ final ByteBuffer bb = ByteBuffer.wrap(serialized);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ bb.position(2);
+ bb.putLong(-100L);
+
+ byte[] newSerialized = bb.array();
+ thrown.expect(ServerStatsDeserializationException.class);
+ thrown.expectMessage("Serialized ServiceStats contains invalid values");
+ ServerStatsEncoding.parseBytes(newSerialized);
+ }
+
+ @Test
+ public void negativeServerLatencyValueTest() throws ServerStatsDeserializationException {
+ ServerStats serverStatsToBeEncoded = null;
+ ServerStats serverStatsDecoded = null;
+ byte[] serialized = null;
+
+ serverStatsToBeEncoded = ServerStats.create(31, 22, (byte) 1);
+ serialized = ServerStatsEncoding.toBytes(serverStatsToBeEncoded);
+
+ // update serialized byte[] with negative value for serviceLatency.
+ final ByteBuffer bb = ByteBuffer.wrap(serialized);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ bb.position(11);
+ bb.putLong(-101L);
+
+ byte[] newSerialized = bb.array();
+ thrown.expect(ServerStatsDeserializationException.class);
+ thrown.expectMessage("Serialized ServiceStats contains invalid values");
+ ServerStatsEncoding.parseBytes(newSerialized);
+ }
+
+ @Test
+ public void emptySerializedBuffer() throws ServerStatsDeserializationException {
+ final ByteBuffer bb = ByteBuffer.allocate(0);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+
+ byte[] newSerialized = bb.array();
+ thrown.expect(ServerStatsDeserializationException.class);
+ thrown.expectMessage("Serialized ServerStats buffer is empty");
+ ServerStatsEncoding.parseBytes(newSerialized);
+ }
+
+ @Test
+ public void invalidNegativeVersion() throws ServerStatsDeserializationException {
+ final ByteBuffer bb = ByteBuffer.allocate(10);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ bb.put((byte) -1);
+ byte[] newSerialized = bb.array();
+ thrown.expect(ServerStatsDeserializationException.class);
+ thrown.expectMessage("Invalid ServerStats version: -1");
+ ServerStatsEncoding.parseBytes(newSerialized);
+ }
+
+ @Test
+ public void invalidCompatibleVersion() throws ServerStatsDeserializationException {
+ final ByteBuffer bb = ByteBuffer.allocate(10);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ bb.put((byte) (ServerStatsEncoding.CURRENT_VERSION + 1));
+ byte[] newSerialized = bb.array();
+ thrown.expect(ServerStatsDeserializationException.class);
+ thrown.expectMessage(
+ "Invalid ServerStats version: " + (ServerStatsEncoding.CURRENT_VERSION + 1));
+ ServerStatsEncoding.parseBytes(newSerialized);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/common/ServerStatsFieldEnumsTest.java b/api/src/test/java/io/opencensus/common/ServerStatsFieldEnumsTest.java
new file mode 100644
index 00000000..ed786f6c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/ServerStatsFieldEnumsTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2018, 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.ServerStatsFieldEnums.Id;
+import io.opencensus.common.ServerStatsFieldEnums.Size;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ServerStatsFieldEnums}. */
+@RunWith(JUnit4.class)
+public class ServerStatsFieldEnumsTest {
+
+ @Test
+ public void enumIdValueOfTest() {
+ assertThat(Id.valueOf(0)).isEqualTo(Id.SERVER_STATS_LB_LATENCY_ID);
+ assertThat(Id.valueOf(1)).isEqualTo(Id.SERVER_STATS_SERVICE_LATENCY_ID);
+ assertThat(Id.valueOf(2)).isEqualTo(Id.SERVER_STATS_TRACE_OPTION_ID);
+ }
+
+ @Test
+ public void enumIdInvalidValueOfTest() {
+ assertThat(Id.valueOf(-1)).isNull();
+ assertThat(Id.valueOf(Id.values().length)).isNull();
+ assertThat(Id.valueOf(Id.values().length + 1)).isNull();
+ }
+
+ @Test
+ public void enumSizeValueTest() {
+ assertThat(Size.SERVER_STATS_LB_LATENCY_SIZE.value()).isEqualTo(8);
+ assertThat(Size.SERVER_STATS_SERVICE_LATENCY_SIZE.value()).isEqualTo(8);
+ assertThat(Size.SERVER_STATS_TRACE_OPTION_SIZE.value()).isEqualTo(1);
+ }
+
+ @Test
+ public void totalSizeTest() {
+ assertThat(ServerStatsFieldEnums.getTotalSize()).isEqualTo(20);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/common/ServerStatsTest.java b/api/src/test/java/io/opencensus/common/ServerStatsTest.java
new file mode 100644
index 00000000..620bbb4f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/ServerStatsTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018, 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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 ServerStats}. */
+@RunWith(JUnit4.class)
+public class ServerStatsTest {
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void serverStatsCreate() {
+ ServerStats serverStats = null;
+
+ serverStats = ServerStats.create(31, 22, (byte) 0);
+ assertThat(serverStats.getLbLatencyNs()).isEqualTo(31);
+ assertThat(serverStats.getServiceLatencyNs()).isEqualTo(22);
+ assertThat(serverStats.getTraceOption()).isEqualTo((byte) 0);
+
+ serverStats = ServerStats.create(1000011L, 900022L, (byte) 1);
+ assertThat(serverStats.getLbLatencyNs()).isEqualTo(1000011L);
+ assertThat(serverStats.getServiceLatencyNs()).isEqualTo(900022L);
+ assertThat(serverStats.getTraceOption()).isEqualTo((byte) 1);
+
+ serverStats = ServerStats.create(0, 22, (byte) 0);
+ assertThat(serverStats.getLbLatencyNs()).isEqualTo(0);
+ assertThat(serverStats.getServiceLatencyNs()).isEqualTo(22);
+ assertThat(serverStats.getTraceOption()).isEqualTo((byte) 0);
+
+ serverStats = ServerStats.create(1010, 0, (byte) 0);
+ assertThat(serverStats.getLbLatencyNs()).isEqualTo(1010);
+ assertThat(serverStats.getServiceLatencyNs()).isEqualTo(0);
+ assertThat(serverStats.getTraceOption()).isEqualTo((byte) 0);
+ }
+
+ @Test
+ public void create_LbLatencyNegative() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'getLbLatencyNs' is less than zero");
+ ServerStats.create(-1L, 100, (byte) 0);
+ }
+
+ @Test
+ public void create_ServerLatencyNegative() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'getServiceLatencyNs' is less than zero");
+ ServerStats.create(100L, -1L, (byte) 0);
+ }
+
+ @Test
+ public void create_LbLatencyAndServerLatencyNegative() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'getLbLatencyNs' is less than zero");
+ ServerStats.create(-100L, -1L, (byte) 0);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/common/TimeUtilsTest.java b/api/src/test/java/io/opencensus/common/TimeUtilsTest.java
new file mode 100644
index 00000000..d6228566
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/TimeUtilsTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018, 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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 TimeUtils}. */
+@RunWith(JUnit4.class)
+public final class TimeUtilsTest {
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void compareLongs() {
+ assertThat(TimeUtils.compareLongs(-1L, 1L)).isLessThan(0);
+ assertThat(TimeUtils.compareLongs(10L, 10L)).isEqualTo(0);
+ assertThat(TimeUtils.compareLongs(1L, 0L)).isGreaterThan(0);
+ }
+
+ @Test
+ public void checkedAdd_TooLow() {
+ thrown.expect(ArithmeticException.class);
+ thrown.expectMessage("Long sum overflow: x=-9223372036854775807, y=-2");
+ TimeUtils.checkedAdd(Long.MIN_VALUE + 1, -2);
+ }
+
+ @Test
+ public void checkedAdd_TooHigh() {
+ thrown.expect(ArithmeticException.class);
+ thrown.expectMessage("Long sum overflow: x=9223372036854775806, y=2");
+ TimeUtils.checkedAdd(Long.MAX_VALUE - 1, 2);
+ }
+
+ @Test
+ public void checkedAdd_Valid() {
+ assertThat(TimeUtils.checkedAdd(1, 2)).isEqualTo(3);
+ assertThat(TimeUtils.checkedAdd(Integer.MAX_VALUE, Integer.MAX_VALUE))
+ .isEqualTo(2L * Integer.MAX_VALUE);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/common/TimestampTest.java b/api/src/test/java/io/opencensus/common/TimestampTest.java
new file mode 100644
index 00000000..b193b3c8
--- /dev/null
+++ b/api/src/test/java/io/opencensus/common/TimestampTest.java
@@ -0,0 +1,217 @@
+/*
+ * 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.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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 Timestamp}. */
+@RunWith(JUnit4.class)
+public class TimestampTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void timestampCreate() {
+ assertThat(Timestamp.create(24, 42).getSeconds()).isEqualTo(24);
+ assertThat(Timestamp.create(24, 42).getNanos()).isEqualTo(42);
+ assertThat(Timestamp.create(-24, 42).getSeconds()).isEqualTo(-24);
+ assertThat(Timestamp.create(-24, 42).getNanos()).isEqualTo(42);
+ assertThat(Timestamp.create(315576000000L, 999999999).getSeconds()).isEqualTo(315576000000L);
+ assertThat(Timestamp.create(315576000000L, 999999999).getNanos()).isEqualTo(999999999);
+ assertThat(Timestamp.create(-315576000000L, 999999999).getSeconds()).isEqualTo(-315576000000L);
+ assertThat(Timestamp.create(-315576000000L, 999999999).getNanos()).isEqualTo(999999999);
+ }
+
+ @Test
+ public void create_SecondsTooLow() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' is less than minimum (-315576000000): -315576000001");
+ Timestamp.create(-315576000001L, 0);
+ }
+
+ @Test
+ public void create_SecondsTooHigh() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' is greater than maximum (315576000000): 315576000001");
+ Timestamp.create(315576000001L, 0);
+ }
+
+ @Test
+ public void create_NanosTooLow_PositiveTime() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'nanos' is less than zero: -1");
+ Timestamp.create(1, -1);
+ }
+
+ @Test
+ public void create_NanosTooHigh_PositiveTime() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'nanos' is greater than maximum (999999999): 1000000000");
+ Timestamp.create(1, 1000000000);
+ }
+
+ @Test
+ public void create_NanosTooLow_NegativeTime() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'nanos' is less than zero: -1");
+ Timestamp.create(-1, -1);
+ }
+
+ @Test
+ public void create_NanosTooHigh_NegativeTime() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'nanos' is greater than maximum (999999999): 1000000000");
+ Timestamp.create(-1, 1000000000);
+ }
+
+ @Test
+ public void timestampFromMillis() {
+ assertThat(Timestamp.fromMillis(0)).isEqualTo(Timestamp.create(0, 0));
+ assertThat(Timestamp.fromMillis(987)).isEqualTo(Timestamp.create(0, 987000000));
+ assertThat(Timestamp.fromMillis(3456)).isEqualTo(Timestamp.create(3, 456000000));
+ }
+
+ @Test
+ public void timestampFromMillis_Negative() {
+ assertThat(Timestamp.fromMillis(-1)).isEqualTo(Timestamp.create(-1, 999000000));
+ assertThat(Timestamp.fromMillis(-999)).isEqualTo(Timestamp.create(-1, 1000000));
+ assertThat(Timestamp.fromMillis(-3456)).isEqualTo(Timestamp.create(-4, 544000000));
+ }
+
+ @Test
+ public void fromMillis_TooLow() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' is less than minimum (-315576000000): -315576000001");
+ Timestamp.fromMillis(-315576000001000L);
+ }
+
+ @Test
+ public void fromMillis_TooHigh() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("'seconds' is greater than maximum (315576000000): 315576000001");
+ Timestamp.fromMillis(315576000001000L);
+ }
+
+ @Test
+ public void timestampAddNanos() {
+ Timestamp timestamp = Timestamp.create(1234, 223);
+ assertThat(timestamp.addNanos(0)).isEqualTo(timestamp);
+ assertThat(timestamp.addNanos(999999777)).isEqualTo(Timestamp.create(1235, 0));
+ assertThat(timestamp.addNanos(1300200500)).isEqualTo(Timestamp.create(1235, 300200723));
+ assertThat(timestamp.addNanos(1999999777)).isEqualTo(Timestamp.create(1236, 0));
+ assertThat(timestamp.addNanos(9876543789L)).isEqualTo(Timestamp.create(1243, 876544012));
+ assertThat(timestamp.addNanos(Long.MAX_VALUE))
+ .isEqualTo(Timestamp.create(1234L + 9223372036L, 223 + 854775807));
+ }
+
+ @Test
+ public void timestampAddNanos_Negative() {
+ Timestamp timestamp = Timestamp.create(1234, 223);
+ assertThat(timestamp.addNanos(-223)).isEqualTo(Timestamp.create(1234, 0));
+ assertThat(timestamp.addNanos(-1000000223)).isEqualTo(Timestamp.create(1233, 0));
+ assertThat(timestamp.addNanos(-1300200500)).isEqualTo(Timestamp.create(1232, 699799723));
+ assertThat(timestamp.addNanos(-4123456213L)).isEqualTo(Timestamp.create(1229, 876544010));
+ assertThat(timestamp.addNanos(Long.MIN_VALUE))
+ .isEqualTo(Timestamp.create(1234L - 9223372036L - 1, 223 + 145224192));
+ }
+
+ @Test
+ public void timestampAddDuration() {
+ Timestamp timestamp = Timestamp.create(1234, 223);
+ assertThat(timestamp.addDuration(Duration.create(1, 0))).isEqualTo(Timestamp.create(1235, 223));
+ assertThat(timestamp.addDuration(Duration.create(0, 1))).isEqualTo(Timestamp.create(1234, 224));
+ assertThat(timestamp.addDuration(Duration.create(1, 1))).isEqualTo(Timestamp.create(1235, 224));
+ assertThat(timestamp.addDuration(Duration.create(1, 999999900)))
+ .isEqualTo(Timestamp.create(1236, 123));
+ }
+
+ @Test
+ public void timestampAddDuration_Negative() {
+ Timestamp timestamp = Timestamp.create(1234, 223);
+ assertThat(timestamp.addDuration(Duration.create(-1234, -223)))
+ .isEqualTo(Timestamp.create(0, 0));
+ assertThat(timestamp.addDuration(Duration.create(-1, 0)))
+ .isEqualTo(Timestamp.create(1233, 223));
+ assertThat(timestamp.addDuration(Duration.create(-1, -1)))
+ .isEqualTo(Timestamp.create(1233, 222));
+ assertThat(timestamp.addDuration(Duration.create(-1, -323)))
+ .isEqualTo(Timestamp.create(1232, 999999900));
+ assertThat(timestamp.addDuration(Duration.create(-33, -999999999)))
+ .isEqualTo(Timestamp.create(1200, 224));
+ }
+
+ @Test
+ public void timestampSubtractTimestamp() {
+ Timestamp timestamp = Timestamp.create(1234, 223);
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(0, 0)))
+ .isEqualTo(Duration.create(1234, 223));
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(1233, 223)))
+ .isEqualTo(Duration.create(1, 0));
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(1233, 222)))
+ .isEqualTo(Duration.create(1, 1));
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(1232, 999999900)))
+ .isEqualTo(Duration.create(1, 323));
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(1200, 224)))
+ .isEqualTo(Duration.create(33, 999999999));
+ }
+
+ @Test
+ public void timestampSubtractTimestamp_NegativeResult() {
+ Timestamp timestamp = Timestamp.create(1234, 223);
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(1235, 223)))
+ .isEqualTo(Duration.create(-1, 0));
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(1234, 224)))
+ .isEqualTo(Duration.create(0, -1));
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(1235, 224)))
+ .isEqualTo(Duration.create(-1, -1));
+ assertThat(timestamp.subtractTimestamp(Timestamp.create(1236, 123)))
+ .isEqualTo(Duration.create(-1, -999999900));
+ }
+
+ @Test
+ public void timestamp_CompareTo() {
+ assertThat(Timestamp.create(0, 0).compareTo(Timestamp.create(0, 0))).isEqualTo(0);
+ assertThat(Timestamp.create(24, 42).compareTo(Timestamp.create(24, 42))).isEqualTo(0);
+ assertThat(Timestamp.create(-24, 42).compareTo(Timestamp.create(-24, 42))).isEqualTo(0);
+ assertThat(Timestamp.create(25, 42).compareTo(Timestamp.create(24, 42))).isEqualTo(1);
+ assertThat(Timestamp.create(24, 45).compareTo(Timestamp.create(24, 42))).isEqualTo(1);
+ assertThat(Timestamp.create(24, 42).compareTo(Timestamp.create(25, 42))).isEqualTo(-1);
+ assertThat(Timestamp.create(24, 42).compareTo(Timestamp.create(24, 45))).isEqualTo(-1);
+ assertThat(Timestamp.create(-25, 42).compareTo(Timestamp.create(-24, 42))).isEqualTo(-1);
+ assertThat(Timestamp.create(-24, 45).compareTo(Timestamp.create(-24, 42))).isEqualTo(1);
+ assertThat(Timestamp.create(-24, 42).compareTo(Timestamp.create(-25, 42))).isEqualTo(1);
+ assertThat(Timestamp.create(-24, 42).compareTo(Timestamp.create(-24, 45))).isEqualTo(-1);
+ }
+
+ @Test
+ public void timestamp_Equal() {
+ // Positive tests.
+ assertThat(Timestamp.create(0, 0)).isEqualTo(Timestamp.create(0, 0));
+ assertThat(Timestamp.create(24, 42)).isEqualTo(Timestamp.create(24, 42));
+ assertThat(Timestamp.create(-24, 42)).isEqualTo(Timestamp.create(-24, 42));
+ // Negative tests.
+ assertThat(Timestamp.create(25, 42)).isNotEqualTo(Timestamp.create(24, 42));
+ assertThat(Timestamp.create(24, 43)).isNotEqualTo(Timestamp.create(24, 42));
+ assertThat(Timestamp.create(-25, 42)).isNotEqualTo(Timestamp.create(-24, 42));
+ assertThat(Timestamp.create(-24, 43)).isNotEqualTo(Timestamp.create(-24, 42));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/internal/ProviderTest.java b/api/src/test/java/io/opencensus/internal/ProviderTest.java
new file mode 100644
index 00000000..1f4c33fa
--- /dev/null
+++ b/api/src/test/java/io/opencensus/internal/ProviderTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ServiceConfigurationError;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Provider}. */
+@RunWith(JUnit4.class)
+public class ProviderTest {
+ static class GoodClass {
+ public GoodClass() {}
+ }
+
+ static class PrivateConstructorClass {
+ private PrivateConstructorClass() {}
+ }
+
+ static class NoDefaultConstructorClass {
+ public NoDefaultConstructorClass(int arg) {}
+ }
+
+ private static class PrivateClass {}
+
+ static interface MyInterface {}
+
+ static class MyInterfaceImpl implements MyInterface {
+ public MyInterfaceImpl() {}
+ }
+
+ @Test(expected = ServiceConfigurationError.class)
+ public void createInstance_ThrowsErrorWhenClassIsPrivate() throws ClassNotFoundException {
+ Provider.createInstance(
+ Class.forName(
+ "io.opencensus.internal.ProviderTest$PrivateClass",
+ true,
+ ProviderTest.class.getClassLoader()),
+ PrivateClass.class);
+ }
+
+ @Test(expected = ServiceConfigurationError.class)
+ public void createInstance_ThrowsErrorWhenClassHasPrivateConstructor()
+ throws ClassNotFoundException {
+ Provider.createInstance(
+ Class.forName(
+ "io.opencensus.internal.ProviderTest$PrivateConstructorClass",
+ true,
+ ProviderTest.class.getClassLoader()),
+ PrivateConstructorClass.class);
+ }
+
+ @Test(expected = ServiceConfigurationError.class)
+ public void createInstance_ThrowsErrorWhenClassDoesNotHaveDefaultConstructor()
+ throws ClassNotFoundException {
+ Provider.createInstance(
+ Class.forName(
+ "io.opencensus.internal.ProviderTest$NoDefaultConstructorClass",
+ true,
+ ProviderTest.class.getClassLoader()),
+ NoDefaultConstructorClass.class);
+ }
+
+ @Test(expected = ServiceConfigurationError.class)
+ public void createInstance_ThrowsErrorWhenClassIsNotASubclass() throws ClassNotFoundException {
+ Provider.createInstance(
+ Class.forName(
+ "io.opencensus.internal.ProviderTest$GoodClass",
+ true,
+ ProviderTest.class.getClassLoader()),
+ MyInterface.class);
+ }
+
+ @Test
+ public void createInstance_GoodClass() throws ClassNotFoundException {
+ assertThat(
+ Provider.createInstance(
+ Class.forName(
+ "io.opencensus.internal.ProviderTest$GoodClass",
+ true,
+ ProviderTest.class.getClassLoader()),
+ GoodClass.class))
+ .isNotNull();
+ }
+
+ @Test
+ public void createInstance_GoodSubclass() throws ClassNotFoundException {
+ assertThat(
+ Provider.createInstance(
+ Class.forName(
+ "io.opencensus.internal.ProviderTest$MyInterfaceImpl",
+ true,
+ ProviderTest.class.getClassLoader()),
+ MyInterface.class))
+ .isNotNull();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/internal/StringUtilsTest.java b/api/src/test/java/io/opencensus/internal/StringUtilsTest.java
new file mode 100644
index 00000000..5e866940
--- /dev/null
+++ b/api/src/test/java/io/opencensus/internal/StringUtilsTest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.internal;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link StringUtils}. */
+@RunWith(JUnit4.class)
+public final class StringUtilsTest {
+
+ @Test
+ public void isPrintableString() {
+ assertTrue(StringUtils.isPrintableString("abcd"));
+ assertFalse(StringUtils.isPrintableString("\2ab\3cd"));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/internal/UtilsTest.java b/api/src/test/java/io/opencensus/internal/UtilsTest.java
new file mode 100644
index 00000000..608a8fe0
--- /dev/null
+++ b/api/src/test/java/io/opencensus/internal/UtilsTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Date;
+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 Utils}. */
+@RunWith(JUnit4.class)
+public final class UtilsTest {
+ private static final String TEST_MESSAGE = "test message";
+ private static final String TEST_MESSAGE_TEMPLATE = "I ate %s eggs.";
+ private static final int TEST_MESSAGE_VALUE = 2;
+ private static final String FORMATED_SIMPLE_TEST_MESSAGE = "I ate 2 eggs.";
+ private static final String FORMATED_COMPLEX_TEST_MESSAGE = "I ate 2 eggs. [2]";
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void checkArgument() {
+ Utils.checkArgument(true, TEST_MESSAGE);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(TEST_MESSAGE);
+ Utils.checkArgument(false, TEST_MESSAGE);
+ }
+
+ @Test
+ public void checkArgument_NullErrorMessage() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("null");
+ Utils.checkArgument(false, null);
+ }
+
+ @Test
+ public void checkArgument_WithSimpleFormat() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(FORMATED_SIMPLE_TEST_MESSAGE);
+ Utils.checkArgument(false, TEST_MESSAGE_TEMPLATE, TEST_MESSAGE_VALUE);
+ }
+
+ @Test
+ public void checkArgument_WithComplexFormat() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(FORMATED_COMPLEX_TEST_MESSAGE);
+ Utils.checkArgument(false, TEST_MESSAGE_TEMPLATE, TEST_MESSAGE_VALUE, TEST_MESSAGE_VALUE);
+ }
+
+ @Test
+ public void checkState() {
+ Utils.checkNotNull(true, TEST_MESSAGE);
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage(TEST_MESSAGE);
+ Utils.checkState(false, TEST_MESSAGE);
+ }
+
+ @Test
+ public void checkState_NullErrorMessage() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("null");
+ Utils.checkState(false, null);
+ }
+
+ @Test
+ public void checkNotNull() {
+ Utils.checkNotNull(new Object(), TEST_MESSAGE);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(TEST_MESSAGE);
+ Utils.checkNotNull(null, TEST_MESSAGE);
+ }
+
+ @Test
+ public void checkNotNull_NullErrorMessage() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("null");
+ Utils.checkNotNull(null, null);
+ }
+
+ @Test
+ public void checkIndex_Valid() {
+ Utils.checkIndex(1, 2);
+ }
+
+ @Test
+ public void checkIndex_NegativeSize() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Negative size: -1");
+ Utils.checkIndex(0, -1);
+ }
+
+ @Test
+ public void checkIndex_NegativeIndex() {
+ thrown.expect(IndexOutOfBoundsException.class);
+ thrown.expectMessage("Index out of bounds: size=10, index=-2");
+ Utils.checkIndex(-2, 10);
+ }
+
+ @Test
+ public void checkIndex_IndexEqualToSize() {
+ thrown.expect(IndexOutOfBoundsException.class);
+ thrown.expectMessage("Index out of bounds: size=5, index=5");
+ Utils.checkIndex(5, 5);
+ }
+
+ @Test
+ public void checkIndex_IndexGreaterThanSize() {
+ thrown.expect(IndexOutOfBoundsException.class);
+ thrown.expectMessage("Index out of bounds: size=10, index=11");
+ Utils.checkIndex(11, 10);
+ }
+
+ @Test
+ public void equalsObjects_Equal() {
+ assertTrue(Utils.equalsObjects(null, null));
+ assertTrue(Utils.equalsObjects(new Date(1L), new Date(1L)));
+ }
+
+ @Test
+ public void equalsObjects_Unequal() {
+ assertFalse(Utils.equalsObjects(null, new Object()));
+ assertFalse(Utils.equalsObjects(new Object(), null));
+ assertFalse(Utils.equalsObjects(new Object(), new Object()));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/DerivedDoubleGaugeTest.java b/api/src/test/java/io/opencensus/metrics/DerivedDoubleGaugeTest.java
new file mode 100644
index 00000000..dbae3c49
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/DerivedDoubleGaugeTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ToDoubleFunction;
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link DerivedDoubleGauge}. */
+// TODO(mayurkale): Add more tests, once DerivedDoubleGauge plugs-in into the registry.
+@RunWith(JUnit4.class)
+public class DerivedDoubleGaugeTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String NAME = "name";
+ private static final String DESCRIPTION = "description";
+ private static final String UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private static final List<LabelValue> EMPTY_LABEL_VALUES = new ArrayList<LabelValue>();
+
+ private final DerivedDoubleGauge derivedDoubleGauge =
+ DerivedDoubleGauge.newNoopDerivedDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ private static final ToDoubleFunction<Object> doubleFunction =
+ new ToDoubleFunction<Object>() {
+ @Override
+ public double applyAsDouble(Object value) {
+ return 5.0;
+ }
+ };
+
+ @Test
+ public void noopCreateTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ derivedDoubleGauge.createTimeSeries(null, null, doubleFunction);
+ }
+
+ @Test
+ public void noopCreateTimeSeries_WithNullElement() {
+ List<LabelValue> labelValues = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValue element should not be null.");
+ derivedDoubleGauge.createTimeSeries(labelValues, null, doubleFunction);
+ }
+
+ @Test
+ public void noopCreateTimeSeries_WithInvalidLabelSize() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Incorrect number of labels.");
+ derivedDoubleGauge.createTimeSeries(EMPTY_LABEL_VALUES, null, doubleFunction);
+ }
+
+ @Test
+ public void createTimeSeries_WithNullFunction() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("function");
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, null);
+ }
+
+ @Test
+ public void noopRemoveTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ derivedDoubleGauge.removeTimeSeries(null);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/DerivedLongGaugeTest.java b/api/src/test/java/io/opencensus/metrics/DerivedLongGaugeTest.java
new file mode 100644
index 00000000..6a462881
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/DerivedLongGaugeTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.ToLongFunction;
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link DerivedLongGauge}. */
+// TODO(mayurkale): Add more tests, once DerivedLongGauge plugs-in into the registry.
+@RunWith(JUnit4.class)
+public class DerivedLongGaugeTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String NAME = "name";
+ private static final String DESCRIPTION = "description";
+ private static final String UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private static final List<LabelValue> EMPTY_LABEL_VALUES = new ArrayList<LabelValue>();
+
+ private final DerivedLongGauge derivedLongGauge =
+ DerivedLongGauge.newNoopDerivedLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ private static final ToLongFunction<Object> longFunction =
+ new ToLongFunction<Object>() {
+ @Override
+ public long applyAsLong(Object value) {
+ return 5;
+ }
+ };
+
+ @Test
+ public void noopCreateTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ derivedLongGauge.createTimeSeries(null, null, longFunction);
+ }
+
+ @Test
+ public void noopCreateTimeSeries_WithNullElement() {
+ List<LabelValue> labelValues = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValue element should not be null.");
+ derivedLongGauge.createTimeSeries(labelValues, null, longFunction);
+ }
+
+ @Test
+ public void noopCreateTimeSeries_WithInvalidLabelSize() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Incorrect number of labels.");
+ derivedLongGauge.createTimeSeries(EMPTY_LABEL_VALUES, null, longFunction);
+ }
+
+ @Test
+ public void createTimeSeries_WithNullFunction() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("function");
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, null, null);
+ }
+
+ @Test
+ public void noopRemoveTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ derivedLongGauge.removeTimeSeries(null);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/DoubleGaugeTest.java b/api/src/test/java/io/opencensus/metrics/DoubleGaugeTest.java
new file mode 100644
index 00000000..b0cdea7c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/DoubleGaugeTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link DoubleGauge}. */
+@RunWith(JUnit4.class)
+public class DoubleGaugeTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String NAME = "name";
+ private static final String DESCRIPTION = "description";
+ private static final String UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private static final List<LabelKey> EMPTY_LABEL_KEYS = new ArrayList<LabelKey>();
+ private static final List<LabelValue> EMPTY_LABEL_VALUES = new ArrayList<LabelValue>();
+
+ // TODO(mayurkale): Add more tests, once DoubleGauge plugs-in into the registry.
+
+ @Test
+ public void noopGetOrCreateTimeSeries_WithNullLabelValues() {
+ DoubleGauge doubleGauge =
+ DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, EMPTY_LABEL_KEYS);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ doubleGauge.getOrCreateTimeSeries(null);
+ }
+
+ @Test
+ public void noopGetOrCreateTimeSeries_WithNullElement() {
+ List<LabelValue> labelValues = Collections.singletonList(null);
+ DoubleGauge doubleGauge = DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValue element should not be null.");
+ doubleGauge.getOrCreateTimeSeries(labelValues);
+ }
+
+ @Test
+ public void noopGetOrCreateTimeSeries_WithInvalidLabelSize() {
+ DoubleGauge doubleGauge = DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Incorrect number of labels.");
+ doubleGauge.getOrCreateTimeSeries(EMPTY_LABEL_VALUES);
+ }
+
+ @Test
+ public void noopRemoveTimeSeries_WithNullLabelValues() {
+ DoubleGauge doubleGauge = DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ doubleGauge.removeTimeSeries(null);
+ }
+
+ @Test
+ public void noopSameAs() {
+ DoubleGauge doubleGauge = DoubleGauge.newNoopDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ assertThat(doubleGauge.getDefaultTimeSeries()).isSameAs(doubleGauge.getDefaultTimeSeries());
+ assertThat(doubleGauge.getDefaultTimeSeries())
+ .isSameAs(doubleGauge.getOrCreateTimeSeries(LABEL_VALUES));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/LabelKeyTest.java b/api/src/test/java/io/opencensus/metrics/LabelKeyTest.java
new file mode 100644
index 00000000..83f2b59a
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/LabelKeyTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link LabelKey}. */
+@RunWith(JUnit4.class)
+public class LabelKeyTest {
+
+ private static final LabelKey KEY = LabelKey.create("key", "description");
+
+ @Test
+ public void testGetKey() {
+ assertThat(KEY.getKey()).isEqualTo("key");
+ }
+
+ @Test
+ public void testGetDescription() {
+ assertThat(KEY.getDescription()).isEqualTo("description");
+ }
+
+ @Test
+ public void create_NoLengthConstraint() {
+ // We have a length constraint of 256-characters for TagKey. That constraint doesn't apply to
+ // LabelKey.
+ char[] chars = new char[300];
+ Arrays.fill(chars, 'k');
+ String key = new String(chars);
+ assertThat(LabelKey.create(key, "").getKey()).isEqualTo(key);
+ }
+
+ @Test
+ public void create_WithUnprintableChars() {
+ String key = "\2ab\3cd";
+ String description = "\4ef\5gh";
+ LabelKey labelKey = LabelKey.create(key, description);
+ assertThat(labelKey.getKey()).isEqualTo(key);
+ assertThat(labelKey.getDescription()).isEqualTo(description);
+ }
+
+ @Test
+ public void create_WithNonAsciiChars() {
+ String key = "键";
+ String description = "测试用键";
+ LabelKey nonAsciiKey = LabelKey.create(key, description);
+ assertThat(nonAsciiKey.getKey()).isEqualTo(key);
+ assertThat(nonAsciiKey.getDescription()).isEqualTo(description);
+ }
+
+ @Test
+ public void create_Empty() {
+ LabelKey emptyKey = LabelKey.create("", "");
+ assertThat(emptyKey.getKey()).isEmpty();
+ assertThat(emptyKey.getDescription()).isEmpty();
+ }
+
+ @Test
+ public void testLabelKeyEquals() {
+ new EqualsTester()
+ .addEqualityGroup(LabelKey.create("foo", ""), LabelKey.create("foo", ""))
+ .addEqualityGroup(LabelKey.create("foo", "description"))
+ .addEqualityGroup(LabelKey.create("bar", ""))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/LabelValueTest.java b/api/src/test/java/io/opencensus/metrics/LabelValueTest.java
new file mode 100644
index 00000000..e5526b2f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/LabelValueTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link LabelValue}. */
+@RunWith(JUnit4.class)
+public class LabelValueTest {
+
+ private static final LabelValue VALUE = LabelValue.create("value");
+ private static final LabelValue UNSET = LabelValue.create(null);
+ private static final LabelValue EMPTY = LabelValue.create("");
+
+ @Test
+ public void testGetValue() {
+ assertThat(VALUE.getValue()).isEqualTo("value");
+ assertThat(UNSET.getValue()).isNull();
+ assertThat(EMPTY.getValue()).isEmpty();
+ }
+
+ @Test
+ public void create_NoLengthConstraint() {
+ // We have a length constraint of 256-characters for TagValue. That constraint doesn't apply to
+ // LabelValue.
+ char[] chars = new char[300];
+ Arrays.fill(chars, 'v');
+ String value = new String(chars);
+ assertThat(LabelValue.create(value).getValue()).isEqualTo(value);
+ }
+
+ @Test
+ public void create_WithUnprintableChars() {
+ String value = "\2ab\3cd";
+ assertThat(LabelValue.create(value).getValue()).isEqualTo(value);
+ }
+
+ @Test
+ public void create_WithNonAsciiChars() {
+ String value = "值";
+ LabelValue nonAsciiValue = LabelValue.create(value);
+ assertThat(nonAsciiValue.getValue()).isEqualTo(value);
+ }
+
+ @Test
+ public void testLabelValueEquals() {
+ new EqualsTester()
+ .addEqualityGroup(LabelValue.create("foo"), LabelValue.create("foo"))
+ .addEqualityGroup(UNSET)
+ .addEqualityGroup(EMPTY)
+ .addEqualityGroup(LabelValue.create("bar"))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/LongGaugeTest.java b/api/src/test/java/io/opencensus/metrics/LongGaugeTest.java
new file mode 100644
index 00000000..eedb287c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/LongGaugeTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link LongGauge}. */
+@RunWith(JUnit4.class)
+public class LongGaugeTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String NAME = "name";
+ private static final String DESCRIPTION = "description";
+ private static final String UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private static final List<LabelKey> EMPTY_LABEL_KEYS = new ArrayList<LabelKey>();
+ private static final List<LabelValue> EMPTY_LABEL_VALUES = new ArrayList<LabelValue>();
+
+ // TODO(mayurkale): Add more tests, once LongGauge plugs-in into the registry.
+
+ @Test
+ public void noopGetOrCreateTimeSeries_WithNullLabelValues() {
+ LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, EMPTY_LABEL_KEYS);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ longGauge.getOrCreateTimeSeries(null);
+ }
+
+ @Test
+ public void noopGetOrCreateTimeSeries_WithNullElement() {
+ List<LabelValue> labelValues = Collections.singletonList(null);
+ LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValue element should not be null.");
+ longGauge.getOrCreateTimeSeries(labelValues);
+ }
+
+ @Test
+ public void noopGetOrCreateTimeSeries_WithInvalidLabelSize() {
+ LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Incorrect number of labels.");
+ longGauge.getOrCreateTimeSeries(EMPTY_LABEL_VALUES);
+ }
+
+ @Test
+ public void noopRemoveTimeSeries_WithNullLabelValues() {
+ LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ longGauge.removeTimeSeries(null);
+ }
+
+ @Test
+ public void noopSameAs() {
+ LongGauge longGauge = LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ assertThat(longGauge.getDefaultTimeSeries()).isSameAs(longGauge.getDefaultTimeSeries());
+ assertThat(longGauge.getDefaultTimeSeries())
+ .isSameAs(longGauge.getOrCreateTimeSeries(LABEL_VALUES));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/MetricRegistryTest.java b/api/src/test/java/io/opencensus/metrics/MetricRegistryTest.java
new file mode 100644
index 00000000..d8a26cc8
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/MetricRegistryTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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;
+
+/** Unit tests for {@link MetricRegistry}. */
+@RunWith(JUnit4.class)
+public class MetricRegistryTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String NAME = "name";
+ private static final String NAME_2 = "name2";
+ private static final String NAME_3 = "name3";
+ private static final String NAME_4 = "name4";
+ private static final String DESCRIPTION = "description";
+ private static final String UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private final MetricRegistry metricRegistry =
+ MetricsComponent.newNoopMetricsComponent().getMetricRegistry();
+
+ @Test
+ public void noopAddLongGauge_NullName() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("name");
+ metricRegistry.addLongGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddLongGauge_NullDescription() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("description");
+ metricRegistry.addLongGauge(NAME, null, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddLongGauge_NullUnit() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("unit");
+ metricRegistry.addLongGauge(NAME, DESCRIPTION, null, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddLongGauge_NullLabels() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKeys");
+ metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, null);
+ }
+
+ @Test
+ public void noopAddLongGauge_WithNullElement() {
+ List<LabelKey> labelKeys = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKey element should not be null.");
+ metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, labelKeys);
+ }
+
+ @Test
+ public void noopAddDoubleGauge_NullName() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("name");
+ metricRegistry.addDoubleGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDoubleGauge_NullDescription() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("description");
+ metricRegistry.addDoubleGauge(NAME_2, null, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDoubleGauge_NullUnit() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("unit");
+ metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, null, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDoubleGauge_NullLabels() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKeys");
+ metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, null);
+ }
+
+ @Test
+ public void noopAddDoubleGauge_WithNullElement() {
+ List<LabelKey> labelKeys = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKey element should not be null.");
+ metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, labelKeys);
+ }
+
+ @Test
+ public void noopAddDerivedLongGauge_NullName() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("name");
+ metricRegistry.addDerivedLongGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDerivedLongGauge_NullDescription() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("description");
+ metricRegistry.addDerivedLongGauge(NAME_3, null, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDerivedLongGauge_NullUnit() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("unit");
+ metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, null, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDerivedLongGauge_NullLabels() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKeys");
+ metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, null);
+ }
+
+ @Test
+ public void noopAddDerivedLongGauge_WithNullElement() {
+ List<LabelKey> labelKeys = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKey element should not be null.");
+ metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, labelKeys);
+ }
+
+ @Test
+ public void noopAddDerivedDoubleGauge_NullName() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("name");
+ metricRegistry.addDerivedDoubleGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDerivedDoubleGauge_NullDescription() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("description");
+ metricRegistry.addDerivedDoubleGauge(NAME_4, null, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDerivedDoubleGauge_NullUnit() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("unit");
+ metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, null, LABEL_KEY);
+ }
+
+ @Test
+ public void noopAddDerivedDoubleGauge_NullLabels() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKeys");
+ metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, null);
+ }
+
+ @Test
+ public void noopAddDerivedDoubleGauge_WithNullElement() {
+ List<LabelKey> labelKeys = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKey element should not be null.");
+ metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, labelKeys);
+ }
+
+ @Test
+ public void noopSameAs() {
+ LongGauge longGauge = metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ assertThat(longGauge.getDefaultTimeSeries()).isSameAs(longGauge.getDefaultTimeSeries());
+ assertThat(longGauge.getDefaultTimeSeries())
+ .isSameAs(longGauge.getOrCreateTimeSeries(LABEL_VALUES));
+
+ DoubleGauge doubleGauge = metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY);
+ assertThat(doubleGauge.getDefaultTimeSeries()).isSameAs(doubleGauge.getDefaultTimeSeries());
+ assertThat(doubleGauge.getDefaultTimeSeries())
+ .isSameAs(doubleGauge.getOrCreateTimeSeries(LABEL_VALUES));
+ }
+
+ @Test
+ public void noopInstanceOf() {
+ assertThat(metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY))
+ .isInstanceOf(LongGauge.newNoopLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY).getClass());
+ assertThat(metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY))
+ .isInstanceOf(
+ DoubleGauge.newNoopDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY).getClass());
+ assertThat(metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY))
+ .isInstanceOf(
+ DerivedLongGauge.newNoopDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY)
+ .getClass());
+ assertThat(metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY))
+ .isInstanceOf(
+ DerivedDoubleGauge.newNoopDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY)
+ .getClass());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/MetricsComponentTest.java b/api/src/test/java/io/opencensus/metrics/MetricsComponentTest.java
new file mode 100644
index 00000000..1c4e70f7
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/MetricsComponentTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.metrics.export.ExportComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MetricsComponent}. */
+@RunWith(JUnit4.class)
+public class MetricsComponentTest {
+ @Test
+ public void defaultExportComponent() {
+ assertThat(MetricsComponent.newNoopMetricsComponent().getExportComponent())
+ .isInstanceOf(ExportComponent.newNoopExportComponent().getClass());
+ }
+
+ @Test
+ public void defaultMetricRegistry() {
+ assertThat(MetricsComponent.newNoopMetricsComponent().getMetricRegistry())
+ .isInstanceOf(MetricRegistry.newNoopMetricRegistry().getClass());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/MetricsTest.java b/api/src/test/java/io/opencensus/metrics/MetricsTest.java
new file mode 100644
index 00000000..9e0eee1f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/MetricsTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.metrics.export.ExportComponent;
+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 Metrics}. */
+@RunWith(JUnit4.class)
+public class MetricsTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void loadMetricsComponent_UsesProvidedClassLoader() {
+ final RuntimeException toThrow = new RuntimeException("UseClassLoader");
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("UseClassLoader");
+ Metrics.loadMetricsComponent(
+ new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) {
+ throw toThrow;
+ }
+ });
+ }
+
+ @Test
+ public void loadMetricsComponent_IgnoresMissingClasses() {
+ ClassLoader classLoader =
+ new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) throws ClassNotFoundException {
+ throw new ClassNotFoundException();
+ }
+ };
+ assertThat(Metrics.loadMetricsComponent(classLoader).getClass().getName())
+ .isEqualTo("io.opencensus.metrics.MetricsComponent$NoopMetricsComponent");
+ }
+
+ @Test
+ public void defaultExportComponent() {
+ assertThat(Metrics.getExportComponent())
+ .isInstanceOf(ExportComponent.newNoopExportComponent().getClass());
+ }
+
+ @Test
+ public void defaultMetricRegistry() {
+ assertThat(Metrics.getMetricRegistry())
+ .isInstanceOf(MetricRegistry.newNoopMetricRegistry().getClass());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/DistributionTest.java b/api/src/test/java/io/opencensus/metrics/export/DistributionTest.java
new file mode 100644
index 00000000..85b31498
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/DistributionTest.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.export.Distribution.Bucket;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import io.opencensus.metrics.export.Distribution.BucketOptions.ExplicitOptions;
+import io.opencensus.metrics.export.Distribution.Exemplar;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+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 Distribution}. */
+@RunWith(JUnit4.class)
+public class DistributionTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final Timestamp TIMESTAMP = Timestamp.create(1, 0);
+ private static final Map<String, String> ATTACHMENTS = Collections.singletonMap("key", "value");
+ private static final double TOLERANCE = 1e-6;
+
+ @Test
+ public void createAndGet_Bucket() {
+ Bucket bucket = Bucket.create(98);
+ assertThat(bucket.getCount()).isEqualTo(98);
+ assertThat(bucket.getExemplar()).isNull();
+ }
+
+ @Test
+ public void createAndGet_BucketWithExemplar() {
+ Exemplar exemplar = Exemplar.create(12.2, TIMESTAMP, ATTACHMENTS);
+ Bucket bucket = Bucket.create(7, exemplar);
+ assertThat(bucket.getCount()).isEqualTo(7);
+ assertThat(bucket.getExemplar()).isEqualTo(exemplar);
+ }
+
+ @Test
+ public void createBucket_preventNullExemplar() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("exemplar");
+ Bucket.create(1, null);
+ }
+
+ @Test
+ public void createAndGet_Exemplar() {
+ Exemplar exemplar = Exemplar.create(-9.9, TIMESTAMP, ATTACHMENTS);
+ assertThat(exemplar.getValue()).isWithin(TOLERANCE).of(-9.9);
+ assertThat(exemplar.getTimestamp()).isEqualTo(TIMESTAMP);
+ assertThat(exemplar.getAttachments()).isEqualTo(ATTACHMENTS);
+ }
+
+ @Test
+ public void createAndGet_ExplicitBuckets() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 3.0);
+
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+ final List<Double> actual = new ArrayList<Double>();
+ bucketOptions.match(
+ new Function<ExplicitOptions, Object>() {
+ @Override
+ public Object apply(ExplicitOptions arg) {
+ actual.addAll(arg.getBucketBoundaries());
+ return null;
+ }
+ },
+ Functions.throwAssertionError());
+
+ assertThat(actual).containsExactlyElementsIn(bucketBounds).inOrder();
+ }
+
+ @Test
+ public void createAndGet_ExplicitBucketsNegativeBounds() {
+ List<Double> bucketBounds = Collections.singletonList(-1.0);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("bucket boundary should be > 0");
+ BucketOptions.explicitOptions(bucketBounds);
+ }
+
+ @Test
+ public void createAndGet_PreventNullExplicitBuckets() {
+ thrown.expect(NullPointerException.class);
+ BucketOptions.explicitOptions(Arrays.asList(1.0, null, 3.0));
+ }
+
+ @Test
+ public void createAndGet_ExplicitBucketsEmptyBounds() {
+ List<Double> bucketBounds = new ArrayList<Double>();
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+
+ final List<Double> actual = new ArrayList<Double>();
+ bucketOptions.match(
+ new Function<ExplicitOptions, Object>() {
+ @Override
+ public Object apply(ExplicitOptions arg) {
+ actual.addAll(arg.getBucketBoundaries());
+ return null;
+ }
+ },
+ Functions.throwAssertionError());
+
+ assertThat(actual).isEmpty();
+ }
+
+ @Test
+ public void createBucketOptions_UnorderedBucketBounds() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 5.0, 2.0);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("bucket boundaries not sorted.");
+ BucketOptions.explicitOptions(bucketBounds);
+ }
+
+ @Test
+ public void createAndGet_PreventNullBucketOptions() {
+ thrown.expect(NullPointerException.class);
+ BucketOptions.explicitOptions(null);
+ }
+
+ @Test
+ public void createAndGet_Distribution() {
+ Exemplar exemplar = Exemplar.create(15.0, TIMESTAMP, ATTACHMENTS);
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+ List<Bucket> buckets =
+ Arrays.asList(
+ Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4, exemplar));
+ Distribution distribution = Distribution.create(10, 6.6, 678.54, bucketOptions, buckets);
+ assertThat(distribution.getCount()).isEqualTo(10);
+ assertThat(distribution.getSum()).isWithin(TOLERANCE).of(6.6);
+ assertThat(distribution.getSumOfSquaredDeviations()).isWithin(TOLERANCE).of(678.54);
+
+ final List<Double> actual = new ArrayList<Double>();
+ distribution
+ .getBucketOptions()
+ .match(
+ new Function<ExplicitOptions, Object>() {
+ @Override
+ public Object apply(ExplicitOptions arg) {
+ actual.addAll(arg.getBucketBoundaries());
+ return null;
+ }
+ },
+ Functions.throwAssertionError());
+
+ assertThat(actual).containsExactlyElementsIn(bucketBounds).inOrder();
+
+ assertThat(distribution.getBuckets()).containsExactlyElementsIn(buckets).inOrder();
+ }
+
+ @Test
+ public void createBucket_NegativeCount() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("bucket count should be non-negative.");
+ Bucket.create(-5);
+ }
+
+ @Test
+ public void createExemplar_PreventNullAttachments() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("attachments");
+ Exemplar.create(15, TIMESTAMP, null);
+ }
+
+ @Test
+ public void createExemplar_PreventNullAttachmentKey() {
+ Map<String, String> attachments = Collections.singletonMap(null, "value");
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("key of attachment");
+ Exemplar.create(15, TIMESTAMP, attachments);
+ }
+
+ @Test
+ public void createExemplar_PreventNullAttachmentValue() {
+ Map<String, String> attachments = Collections.singletonMap("key", null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("value of attachment");
+ Exemplar.create(15, TIMESTAMP, attachments);
+ }
+
+ @Test
+ public void createDistribution_NegativeCount() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+
+ List<Bucket> buckets =
+ Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4));
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("count should be non-negative.");
+ Distribution.create(-10, 6.6, 678.54, bucketOptions, buckets);
+ }
+
+ @Test
+ public void createDistribution_NegativeSumOfSquaredDeviations() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+
+ List<Bucket> buckets =
+ Arrays.asList(Bucket.create(0), Bucket.create(0), Bucket.create(0), Bucket.create(0));
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("sum of squared deviations should be non-negative.");
+ Distribution.create(0, 6.6, -678.54, bucketOptions, buckets);
+ }
+
+ @Test
+ public void createDistribution_ZeroCountAndPositiveMean() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+
+ List<Bucket> buckets =
+ Arrays.asList(Bucket.create(0), Bucket.create(0), Bucket.create(0), Bucket.create(0));
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("sum should be 0 if count is 0.");
+ Distribution.create(0, 6.6, 0, bucketOptions, buckets);
+ }
+
+ @Test
+ public void createDistribution_ZeroCountAndSumOfSquaredDeviations() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+ List<Bucket> buckets =
+ Arrays.asList(Bucket.create(0), Bucket.create(0), Bucket.create(0), Bucket.create(0));
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("sum of squared deviations should be 0 if count is 0.");
+ Distribution.create(0, 0, 678.54, bucketOptions, buckets);
+ }
+
+ @Test
+ public void createDistribution_NullBucketBoundaries() {
+ List<Bucket> buckets =
+ Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4));
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("bucketBoundaries");
+ Distribution.create(10, 6.6, 678.54, BucketOptions.explicitOptions(null), buckets);
+ }
+
+ @Test
+ public void createDistribution_NullBucketBoundary() {
+ List<Bucket> buckets =
+ Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4));
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("bucketBoundary");
+ Distribution.create(
+ 10, 6.6, 678.54, BucketOptions.explicitOptions(Arrays.asList(2.5, null)), buckets);
+ }
+
+ @Test
+ public void createDistribution_NullBucketOptions() {
+ List<Bucket> buckets =
+ Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4));
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("bucketOptions");
+ Distribution.create(10, 6.6, 678.54, null, buckets);
+ }
+
+ @Test
+ public void createDistribution_NullBucketList() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("buckets");
+ Distribution.create(10, 6.6, 678.54, bucketOptions, null);
+ }
+
+ @Test
+ public void createDistribution_NullBucket() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 5.0);
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBounds);
+ List<Bucket> buckets =
+ Arrays.asList(Bucket.create(3), Bucket.create(1), null, Bucket.create(4));
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("bucket");
+ Distribution.create(10, 6.6, 678.54, bucketOptions, buckets);
+ }
+
+ @Test
+ public void testEquals() {
+ List<Double> bucketBounds = Arrays.asList(1.0, 2.0, 2.5);
+ new EqualsTester()
+ .addEqualityGroup(
+ Distribution.create(
+ 10,
+ 10,
+ 1,
+ BucketOptions.explicitOptions(bucketBounds),
+ Arrays.asList(
+ Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4))),
+ Distribution.create(
+ 10,
+ 10,
+ 1,
+ BucketOptions.explicitOptions(bucketBounds),
+ Arrays.asList(
+ Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4))))
+ .addEqualityGroup(
+ Distribution.create(
+ 7,
+ 10,
+ 23.456,
+ BucketOptions.explicitOptions(bucketBounds),
+ Arrays.asList(
+ Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4))))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/ExportComponentTest.java b/api/src/test/java/io/opencensus/metrics/export/ExportComponentTest.java
new file mode 100644
index 00000000..15c6e883
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/ExportComponentTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExportComponent}. */
+@RunWith(JUnit4.class)
+public class ExportComponentTest {
+ @Test
+ public void defaultMetricExporter() {
+ assertThat(ExportComponent.newNoopExportComponent().getMetricProducerManager())
+ .isInstanceOf(MetricProducerManager.class);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/MetricDescriptorTest.java b/api/src/test/java/io/opencensus/metrics/export/MetricDescriptorTest.java
new file mode 100644
index 00000000..502170c6
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/MetricDescriptorTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import java.util.Arrays;
+import java.util.List;
+import org.hamcrest.CoreMatchers;
+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 MetricDescriptor}. */
+@RunWith(JUnit4.class)
+public class MetricDescriptorTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final String METRIC_NAME_1 = "metric1";
+ private static final String METRIC_NAME_2 = "metric2";
+ private static final String DESCRIPTION = "Metric description.";
+ private static final String UNIT = "kb/s";
+ private static final LabelKey KEY_1 = LabelKey.create("key1", "some key");
+ private static final LabelKey KEY_2 = LabelKey.create("key2", "some other key");
+
+ @Test
+ public void testGet() {
+ MetricDescriptor metricDescriptor =
+ MetricDescriptor.create(
+ METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2));
+ assertThat(metricDescriptor.getName()).isEqualTo(METRIC_NAME_1);
+ assertThat(metricDescriptor.getDescription()).isEqualTo(DESCRIPTION);
+ assertThat(metricDescriptor.getUnit()).isEqualTo(UNIT);
+ assertThat(metricDescriptor.getType()).isEqualTo(Type.GAUGE_DOUBLE);
+ assertThat(metricDescriptor.getLabelKeys()).containsExactly(KEY_1, KEY_2).inOrder();
+ }
+
+ @Test
+ public void preventNullLabelKeyList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("labelKeys"));
+ MetricDescriptor.create(METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, null);
+ }
+
+ @Test
+ public void preventNullLabelKey() {
+ List<LabelKey> keys = Arrays.asList(KEY_1, null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("labelKey"));
+ MetricDescriptor.create(METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, keys);
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ MetricDescriptor.create(
+ METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2)),
+ MetricDescriptor.create(
+ METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2)))
+ .addEqualityGroup(
+ MetricDescriptor.create(
+ METRIC_NAME_2, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2)))
+ .addEqualityGroup(
+ MetricDescriptor.create(
+ METRIC_NAME_2, DESCRIPTION, UNIT, Type.GAUGE_INT64, Arrays.asList(KEY_1, KEY_2)))
+ .addEqualityGroup(
+ MetricDescriptor.create(
+ METRIC_NAME_1,
+ DESCRIPTION,
+ UNIT,
+ Type.CUMULATIVE_DISTRIBUTION,
+ Arrays.asList(KEY_1, KEY_2)))
+ .addEqualityGroup(
+ MetricDescriptor.create(
+ METRIC_NAME_1,
+ DESCRIPTION,
+ UNIT,
+ Type.CUMULATIVE_DISTRIBUTION,
+ Arrays.asList(KEY_1)))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/MetricProducerManagerTest.java b/api/src/test/java/io/opencensus/metrics/export/MetricProducerManagerTest.java
new file mode 100644
index 00000000..1025427f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/MetricProducerManagerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link MetricProducerManager}. */
+@RunWith(JUnit4.class)
+public class MetricProducerManagerTest {
+ private final MetricProducerManager metricProducerManager =
+ MetricProducerManager.newNoopMetricProducerManager();
+ @Mock private MetricProducer metricProducer;
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void add_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ metricProducerManager.add(null);
+ }
+
+ @Test
+ public void add() {
+ metricProducerManager.add(metricProducer);
+ assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+ }
+
+ @Test
+ public void addAndRemove() {
+ metricProducerManager.add(metricProducer);
+ assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+ metricProducerManager.remove(metricProducer);
+ assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+ }
+
+ @Test
+ public void remove_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ metricProducerManager.remove(null);
+ }
+
+ @Test
+ public void remove_FromEmpty() {
+ metricProducerManager.remove(metricProducer);
+ assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+ }
+
+ @Test
+ public void getAllMetricProducer_empty() {
+ assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/MetricTest.java b/api/src/test/java/io/opencensus/metrics/export/MetricTest.java
new file mode 100644
index 00000000..ed205289
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/MetricTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link Metric}. */
+@RunWith(JUnit4.class)
+public class MetricTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final String METRIC_NAME_1 = "metric1";
+ private static final String METRIC_NAME_2 = "metric2";
+ private static final String DESCRIPTION = "Metric description.";
+ private static final String UNIT = "kb/s";
+ private static final LabelKey KEY_1 = LabelKey.create("key1", "some key");
+ private static final LabelKey KEY_2 = LabelKey.create("key1", "some other key");
+ private static final MetricDescriptor METRIC_DESCRIPTOR_1 =
+ MetricDescriptor.create(
+ METRIC_NAME_1, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, Arrays.asList(KEY_1, KEY_2));
+ private static final MetricDescriptor METRIC_DESCRIPTOR_2 =
+ MetricDescriptor.create(
+ METRIC_NAME_2,
+ DESCRIPTION,
+ UNIT,
+ Type.CUMULATIVE_INT64,
+ Collections.singletonList(KEY_1));
+ private static final LabelValue LABEL_VALUE_1 = LabelValue.create("value1");
+ private static final LabelValue LABEL_VALUE_2 = LabelValue.create("value1");
+ private static final LabelValue LABEL_VALUE_EMPTY = LabelValue.create("");
+ private static final Value VALUE_LONG = Value.longValue(12345678);
+ private static final Value VALUE_DOUBLE_1 = Value.doubleValue(-345.77);
+ private static final Value VALUE_DOUBLE_2 = Value.doubleValue(133.79);
+ private static final Timestamp TIMESTAMP_1 = Timestamp.fromMillis(1000);
+ private static final Timestamp TIMESTAMP_2 = Timestamp.fromMillis(2000);
+ private static final Timestamp TIMESTAMP_3 = Timestamp.fromMillis(3000);
+ private static final Point POINT_1 = Point.create(VALUE_DOUBLE_1, TIMESTAMP_2);
+ private static final Point POINT_2 = Point.create(VALUE_DOUBLE_2, TIMESTAMP_3);
+ private static final Point POINT_3 = Point.create(VALUE_LONG, TIMESTAMP_3);
+ private static final TimeSeries GAUGE_TIME_SERIES_1 =
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Collections.singletonList(POINT_1), null);
+ private static final TimeSeries GAUGE_TIME_SERIES_2 =
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Collections.singletonList(POINT_2), null);
+ private static final TimeSeries CUMULATIVE_TIME_SERIES =
+ TimeSeries.create(
+ Collections.singletonList(LABEL_VALUE_EMPTY),
+ Collections.singletonList(POINT_3),
+ TIMESTAMP_1);
+
+ @Test
+ public void testGet() {
+ Metric metric =
+ Metric.create(METRIC_DESCRIPTOR_1, Arrays.asList(GAUGE_TIME_SERIES_1, GAUGE_TIME_SERIES_2));
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR_1);
+ assertThat(metric.getTimeSeriesList())
+ .containsExactly(GAUGE_TIME_SERIES_1, GAUGE_TIME_SERIES_2)
+ .inOrder();
+ }
+
+ @Test
+ public void typeMismatch_GaugeDouble_Long() {
+ typeMismatch(
+ METRIC_DESCRIPTOR_1,
+ Collections.singletonList(CUMULATIVE_TIME_SERIES),
+ String.format("Type mismatch: %s, %s.", Type.GAUGE_DOUBLE, "ValueLong"));
+ }
+
+ @Test
+ public void typeMismatch_CumulativeInt64_Double() {
+ typeMismatch(
+ METRIC_DESCRIPTOR_2,
+ Collections.singletonList(GAUGE_TIME_SERIES_1),
+ String.format("Type mismatch: %s, %s.", Type.CUMULATIVE_INT64, "ValueDouble"));
+ }
+
+ private void typeMismatch(
+ MetricDescriptor metricDescriptor, List<TimeSeries> timeSeriesList, String errorMessage) {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(errorMessage);
+ Metric.create(metricDescriptor, timeSeriesList);
+ }
+
+ @Test
+ public void create_WithNullMetricDescriptor() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("metricDescriptor");
+ Metric.create(null, Collections.<TimeSeries>emptyList());
+ }
+
+ @Test
+ public void create_WithNullTimeSeriesList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("timeSeriesList");
+ Metric.create(METRIC_DESCRIPTOR_1, null);
+ }
+
+ @Test
+ public void create_WithNullTimeSeries() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("timeSeries");
+ Metric.create(METRIC_DESCRIPTOR_1, Arrays.asList(GAUGE_TIME_SERIES_1, null));
+ }
+
+ @Test
+ public void immutableTimeSeriesList() {
+ List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>();
+ timeSeriesList.add(GAUGE_TIME_SERIES_1);
+ Metric metric = Metric.create(METRIC_DESCRIPTOR_1, timeSeriesList);
+ timeSeriesList.add(GAUGE_TIME_SERIES_2);
+ assertThat(metric.getTimeSeriesList()).containsExactly(GAUGE_TIME_SERIES_1);
+ }
+
+ @Test
+ public void createWithOneTimeSeries_WithNullTimeSeries() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("timeSeries");
+ Metric.createWithOneTimeSeries(METRIC_DESCRIPTOR_1, null);
+ }
+
+ @Test
+ public void createWithOneTimeSeries_WithNullMetricDescriptor() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("metricDescriptor");
+ Metric.createWithOneTimeSeries(null, GAUGE_TIME_SERIES_1);
+ }
+
+ @Test
+ public void testGet_WithOneTimeSeries() {
+ Metric metric = Metric.createWithOneTimeSeries(METRIC_DESCRIPTOR_1, GAUGE_TIME_SERIES_1);
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR_1);
+ assertThat(metric.getTimeSeriesList()).containsExactly(GAUGE_TIME_SERIES_1);
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ Metric.create(
+ METRIC_DESCRIPTOR_1, Arrays.asList(GAUGE_TIME_SERIES_1, GAUGE_TIME_SERIES_2)),
+ Metric.create(
+ METRIC_DESCRIPTOR_1, Arrays.asList(GAUGE_TIME_SERIES_1, GAUGE_TIME_SERIES_2)))
+ .addEqualityGroup(Metric.create(METRIC_DESCRIPTOR_1, Collections.<TimeSeries>emptyList()))
+ .addEqualityGroup(
+ Metric.createWithOneTimeSeries(METRIC_DESCRIPTOR_2, CUMULATIVE_TIME_SERIES))
+ .addEqualityGroup(Metric.create(METRIC_DESCRIPTOR_2, Collections.<TimeSeries>emptyList()))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/PointTest.java b/api/src/test/java/io/opencensus/metrics/export/PointTest.java
new file mode 100644
index 00000000..cdfc7792
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/PointTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.export.Distribution.Bucket;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Point}. */
+@RunWith(JUnit4.class)
+public class PointTest {
+
+ private static final Value DOUBLE_VALUE = Value.doubleValue(55.5);
+ private static final Value LONG_VALUE = Value.longValue(9876543210L);
+ private static final Value DISTRIBUTION_VALUE =
+ Value.distributionValue(
+ Distribution.create(
+ 10,
+ 6.6,
+ 678.54,
+ BucketOptions.explicitOptions(Arrays.asList(1.0, 2.0, 5.0)),
+ Arrays.asList(
+ Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4))));
+ private static final Timestamp TIMESTAMP_1 = Timestamp.create(1, 2);
+ private static final Timestamp TIMESTAMP_2 = Timestamp.create(3, 4);
+ private static final Timestamp TIMESTAMP_3 = Timestamp.create(5, 6);
+
+ @Test
+ public void testGet() {
+ Point point = Point.create(DOUBLE_VALUE, TIMESTAMP_1);
+ assertThat(point.getValue()).isEqualTo(DOUBLE_VALUE);
+ assertThat(point.getTimestamp()).isEqualTo(TIMESTAMP_1);
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ Point.create(DOUBLE_VALUE, TIMESTAMP_1), Point.create(DOUBLE_VALUE, TIMESTAMP_1))
+ .addEqualityGroup(Point.create(LONG_VALUE, TIMESTAMP_1))
+ .addEqualityGroup(Point.create(LONG_VALUE, TIMESTAMP_2))
+ .addEqualityGroup(
+ Point.create(DISTRIBUTION_VALUE, TIMESTAMP_2),
+ Point.create(DISTRIBUTION_VALUE, TIMESTAMP_2))
+ .addEqualityGroup(Point.create(DISTRIBUTION_VALUE, TIMESTAMP_3))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/SummaryTest.java b/api/src/test/java/io/opencensus/metrics/export/SummaryTest.java
new file mode 100644
index 00000000..c10df043
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/SummaryTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.metrics.export.Summary.Snapshot;
+import io.opencensus.metrics.export.Summary.Snapshot.ValueAtPercentile;
+import java.util.Collections;
+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 Summary}. */
+@RunWith(JUnit4.class)
+public class SummaryTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+ private static final double TOLERANCE = 1e-6;
+
+ @Test
+ public void createAndGet_ValueAtPercentile() {
+ ValueAtPercentile valueAtPercentile = ValueAtPercentile.create(99.5, 10.2);
+ assertThat(valueAtPercentile.getPercentile()).isWithin(TOLERANCE).of(99.5);
+ assertThat(valueAtPercentile.getValue()).isWithin(TOLERANCE).of(10.2);
+ }
+
+ @Test
+ public void createValueAtPercentile_InvalidValueAtPercentileInterval() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("percentile must be in the interval (0.0, 100.0]");
+ ValueAtPercentile.create(100.1, 10.2);
+ }
+
+ @Test
+ public void createValueAtPercentile_NegativeValue() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("value must be non-negative");
+ ValueAtPercentile.create(99.5, -10.2);
+ }
+
+ @Test
+ public void createAndGet_Snapshot() {
+ Snapshot snapshot =
+ Snapshot.create(
+ 10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+ assertThat(snapshot.getCount()).isEqualTo(10);
+ assertThat(snapshot.getSum()).isWithin(TOLERANCE).of(87.07);
+ assertThat(snapshot.getValueAtPercentiles())
+ .containsExactly(ValueAtPercentile.create(99.5, 10.2));
+ }
+
+ @Test
+ public void createAndGet_Snapshot_WithNullCountAndSum() {
+ Snapshot snapshot =
+ Snapshot.create(
+ null, null, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+ assertThat(snapshot.getCount()).isNull();
+ assertThat(snapshot.getSum()).isNull();
+ assertThat(snapshot.getValueAtPercentiles())
+ .containsExactly(ValueAtPercentile.create(99.5, 10.2));
+ }
+
+ @Test
+ public void createSnapshot_NegativeCount() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("count must be non-negative");
+ Snapshot.create(-10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+ }
+
+ @Test
+ public void createSnapshot_NegativeSum() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("sum must be non-negative");
+ Snapshot.create(10L, -87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+ }
+
+ @Test
+ public void createSnapshot_ZeroCountAndNonZeroSum() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("sum must be 0 if count is 0");
+ Snapshot.create(0L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+ }
+
+ @Test
+ public void createSnapshot_NullValueAtPercentilesList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("valueAtPercentiles");
+ Snapshot.create(10L, 87.07, null);
+ }
+
+ @Test
+ public void createSnapshot_OneNullValueAtPercentile() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("value in valueAtPercentiles");
+ Snapshot.create(10L, 87.07, Collections.<ValueAtPercentile>singletonList(null));
+ }
+
+ @Test
+ public void createAndGet_Summary() {
+ Snapshot snapshot =
+ Snapshot.create(
+ 10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)));
+ Summary summary = Summary.create(10L, 6.6, snapshot);
+ assertThat(summary.getCount()).isEqualTo(10);
+ assertThat(summary.getSum()).isWithin(TOLERANCE).of(6.6);
+ assertThat(summary.getSnapshot()).isEqualTo(snapshot);
+ }
+
+ @Test
+ public void createSummary_NegativeCount() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("count must be non-negative");
+ Summary.create(
+ -10L, 6.6, Snapshot.create(null, null, Collections.<ValueAtPercentile>emptyList()));
+ }
+
+ @Test
+ public void createSummary_NegativeSum() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("sum must be non-negative");
+ Summary.create(
+ 10L, -6.6, Snapshot.create(null, null, Collections.<ValueAtPercentile>emptyList()));
+ }
+
+ @Test
+ public void createSummary_ZeroCountAndNonZeroSum() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("sum must be 0 if count is 0");
+ Summary.create(
+ 0L, 6.6, Snapshot.create(null, null, Collections.<ValueAtPercentile>emptyList()));
+ }
+
+ @Test
+ public void createSummary_NullSnapshot() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("snapshot");
+ Summary.create(10L, 6.6, null);
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ Summary.create(
+ 10L,
+ 10.0,
+ Snapshot.create(
+ 10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)))),
+ Summary.create(
+ 10L,
+ 10.0,
+ Snapshot.create(
+ 10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)))))
+ .addEqualityGroup(
+ Summary.create(
+ 7L,
+ 10.0,
+ Snapshot.create(
+ 10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)))))
+ .addEqualityGroup(
+ Summary.create(
+ 10L,
+ 7.0,
+ Snapshot.create(
+ 10L, 87.07, Collections.singletonList(ValueAtPercentile.create(99.5, 10.2)))))
+ .addEqualityGroup(
+ Summary.create(
+ 10L, 10.0, Snapshot.create(null, null, Collections.<ValueAtPercentile>emptyList())))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/TimeSeriesTest.java b/api/src/test/java/io/opencensus/metrics/export/TimeSeriesTest.java
new file mode 100644
index 00000000..92a2c8cf
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/TimeSeriesTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelValue;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.hamcrest.CoreMatchers;
+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 TimeSeries}. */
+@RunWith(JUnit4.class)
+public class TimeSeriesTest {
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final LabelValue LABEL_VALUE_1 = LabelValue.create("value1");
+ private static final LabelValue LABEL_VALUE_2 = LabelValue.create("value2");
+ private static final Value VALUE_LONG = Value.longValue(12345678);
+ private static final Value VALUE_DOUBLE = Value.doubleValue(-345.77);
+ private static final Timestamp TIMESTAMP_1 = Timestamp.fromMillis(1000);
+ private static final Timestamp TIMESTAMP_2 = Timestamp.fromMillis(2000);
+ private static final Timestamp TIMESTAMP_3 = Timestamp.fromMillis(3000);
+ private static final Point POINT_1 = Point.create(VALUE_DOUBLE, TIMESTAMP_2);
+ private static final Point POINT_2 = Point.create(VALUE_LONG, TIMESTAMP_3);
+
+ @Test
+ public void testGet_TimeSeries() {
+ TimeSeries cumulativeTimeSeries =
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), TIMESTAMP_1);
+ assertThat(cumulativeTimeSeries.getStartTimestamp()).isEqualTo(TIMESTAMP_1);
+ assertThat(cumulativeTimeSeries.getLabelValues())
+ .containsExactly(LABEL_VALUE_1, LABEL_VALUE_2)
+ .inOrder();
+ assertThat(cumulativeTimeSeries.getPoints()).containsExactly(POINT_1).inOrder();
+ }
+
+ @Test
+ public void create_WithNullLabelValueList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("labelValues"));
+ TimeSeries.create(null, Collections.<Point>emptyList(), TIMESTAMP_1);
+ }
+
+ @Test
+ public void create_WithNullLabelValue() {
+ List<LabelValue> labelValues = Arrays.asList(LABEL_VALUE_1, null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("labelValue"));
+ TimeSeries.create(labelValues, Collections.<Point>emptyList(), TIMESTAMP_1);
+ }
+
+ @Test
+ public void create_WithNullPointList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("points"));
+ TimeSeries.create(Collections.<LabelValue>emptyList(), null, TIMESTAMP_1);
+ }
+
+ @Test
+ public void create_WithNullPoint() {
+ List<Point> points = Arrays.asList(POINT_1, null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("point"));
+ TimeSeries.create(Collections.<LabelValue>emptyList(), points, TIMESTAMP_1);
+ }
+
+ @Test
+ public void testGet_WithOnePointTimeSeries() {
+ TimeSeries cumulativeTimeSeries =
+ TimeSeries.createWithOnePoint(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), POINT_1, TIMESTAMP_1);
+ assertThat(cumulativeTimeSeries.getStartTimestamp()).isEqualTo(TIMESTAMP_1);
+ assertThat(cumulativeTimeSeries.getLabelValues())
+ .containsExactly(LABEL_VALUE_1, LABEL_VALUE_2)
+ .inOrder();
+ assertThat(cumulativeTimeSeries.getPoints()).containsExactly(POINT_1).inOrder();
+ }
+
+ @Test
+ public void createWithOnePoint_WithNullLabelValueList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("labelValues"));
+ TimeSeries.createWithOnePoint(null, POINT_1, TIMESTAMP_1);
+ }
+
+ @Test
+ public void createWithOnePoint_WithNullLabelValue() {
+ List<LabelValue> labelValues = Arrays.asList(LABEL_VALUE_1, null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("labelValue"));
+ TimeSeries.createWithOnePoint(labelValues, POINT_1, TIMESTAMP_1);
+ }
+
+ @Test
+ public void createWithOnePoint_WithNullPointList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(CoreMatchers.equalTo("point"));
+ TimeSeries.createWithOnePoint(Collections.<LabelValue>emptyList(), null, TIMESTAMP_1);
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), TIMESTAMP_1),
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), TIMESTAMP_1))
+ .addEqualityGroup(
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), null),
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), null))
+ .addEqualityGroup(
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1, LABEL_VALUE_2), Arrays.asList(POINT_1), TIMESTAMP_2))
+ .addEqualityGroup(
+ TimeSeries.create(Arrays.asList(LABEL_VALUE_1), Arrays.asList(POINT_1), TIMESTAMP_2))
+ .addEqualityGroup(
+ TimeSeries.create(Arrays.asList(LABEL_VALUE_1), Arrays.asList(POINT_2), TIMESTAMP_2))
+ .addEqualityGroup(
+ TimeSeries.create(
+ Arrays.asList(LABEL_VALUE_1), Arrays.asList(POINT_1, POINT_2), TIMESTAMP_2))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/metrics/export/ValueTest.java b/api/src/test/java/io/opencensus/metrics/export/ValueTest.java
new file mode 100644
index 00000000..bf947692
--- /dev/null
+++ b/api/src/test/java/io/opencensus/metrics/export/ValueTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.metrics.export.Distribution.Bucket;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import io.opencensus.metrics.export.Distribution.BucketOptions.ExplicitOptions;
+import io.opencensus.metrics.export.Summary.Snapshot;
+import io.opencensus.metrics.export.Summary.Snapshot.ValueAtPercentile;
+import io.opencensus.metrics.export.Value.ValueDistribution;
+import io.opencensus.metrics.export.Value.ValueDouble;
+import io.opencensus.metrics.export.Value.ValueLong;
+import io.opencensus.metrics.export.Value.ValueSummary;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Value}. */
+@RunWith(JUnit4.class)
+public class ValueTest {
+ private static final double TOLERANCE = 1e-6;
+
+ private static final Distribution DISTRIBUTION =
+ Distribution.create(
+ 10,
+ 10,
+ 1,
+ BucketOptions.explicitOptions(Arrays.asList(1.0, 2.0, 5.0)),
+ Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4)));
+ private static final Summary SUMMARY =
+ Summary.create(
+ 10L,
+ 10.0,
+ Snapshot.create(
+ 10L, 87.07, Collections.singletonList(ValueAtPercentile.create(0.98, 10.2))));
+
+ @Test
+ public void createAndGet_ValueDouble() {
+ Value value = Value.doubleValue(-34.56);
+ assertThat(value).isInstanceOf(ValueDouble.class);
+ assertThat(((ValueDouble) value).getValue()).isWithin(TOLERANCE).of(-34.56);
+ }
+
+ @Test
+ public void createAndGet_ValueLong() {
+ Value value = Value.longValue(123456789);
+ assertThat(value).isInstanceOf(ValueLong.class);
+ assertThat(((ValueLong) value).getValue()).isEqualTo(123456789);
+ }
+
+ @Test
+ public void createAndGet_ValueDistribution() {
+ Value value = Value.distributionValue(DISTRIBUTION);
+ assertThat(value).isInstanceOf(ValueDistribution.class);
+ assertThat(((ValueDistribution) value).getValue()).isEqualTo(DISTRIBUTION);
+ }
+
+ @Test
+ public void createAndGet_ValueSummary() {
+ Value value = Value.summaryValue(SUMMARY);
+ assertThat(value).isInstanceOf(ValueSummary.class);
+ assertThat(((ValueSummary) value).getValue()).isEqualTo(SUMMARY);
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(Value.doubleValue(1.0), Value.doubleValue(1.0))
+ .addEqualityGroup(Value.doubleValue(2.0))
+ .addEqualityGroup(Value.longValue(1L))
+ .addEqualityGroup(Value.longValue(2L))
+ .addEqualityGroup(
+ Value.distributionValue(
+ Distribution.create(
+ 7,
+ 10,
+ 23.456,
+ BucketOptions.explicitOptions(Arrays.asList(1.0, 2.0, 5.0)),
+ Arrays.asList(
+ Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4)))))
+ .testEquals();
+ }
+
+ @Test
+ public void testMatch() {
+ List<Value> values =
+ Arrays.asList(
+ ValueDouble.create(1.0),
+ ValueLong.create(-1),
+ ValueDistribution.create(DISTRIBUTION),
+ ValueSummary.create(SUMMARY));
+ List<Number> expected =
+ Arrays.<Number>asList(1.0, -1L, 10.0, 10L, 1.0, 1.0, 2.0, 5.0, 3L, 1L, 2L, 4L);
+ final List<Number> actual = new ArrayList<Number>();
+ for (Value value : values) {
+ value.match(
+ new Function<Double, Object>() {
+ @Override
+ public Object apply(Double arg) {
+ actual.add(arg);
+ return null;
+ }
+ },
+ new Function<Long, Object>() {
+ @Override
+ public Object apply(Long arg) {
+ actual.add(arg);
+ return null;
+ }
+ },
+ new Function<Distribution, Object>() {
+ @Override
+ public Object apply(Distribution arg) {
+ actual.add(arg.getSum());
+ actual.add(arg.getCount());
+ actual.add(arg.getSumOfSquaredDeviations());
+
+ arg.getBucketOptions()
+ .match(
+ new Function<ExplicitOptions, Object>() {
+ @Override
+ public Object apply(ExplicitOptions arg) {
+ actual.addAll(arg.getBucketBoundaries());
+ return null;
+ }
+ },
+ Functions.throwAssertionError());
+
+ for (Bucket bucket : arg.getBuckets()) {
+ actual.add(bucket.getCount());
+ }
+ return null;
+ }
+ },
+ new Function<Summary, Object>() {
+ @Override
+ public Object apply(Summary arg) {
+ return null;
+ }
+ },
+ Functions.throwAssertionError());
+ }
+ assertThat(actual).containsExactlyElementsIn(expected).inOrder();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/stats/AggregationDataTest.java b/api/src/test/java/io/opencensus/stats/AggregationDataTest.java
new file mode 100644
index 00000000..a6d6d1de
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/AggregationDataTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+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.stats.AggregationData}. */
+@RunWith(JUnit4.class)
+public class AggregationDataTest {
+
+ private static final double TOLERANCE = 1e-6;
+ private static final Timestamp TIMESTAMP_1 = Timestamp.create(1, 0);
+ private static final Timestamp TIMESTAMP_2 = Timestamp.create(2, 0);
+ private static final Map<String, String> ATTACHMENTS = Collections.singletonMap("key", "value");
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testCreateDistributionData() {
+ DistributionData distributionData =
+ DistributionData.create(7.7, 10, 1.1, 9.9, 32.2, Arrays.asList(4L, 1L, 5L));
+ assertThat(distributionData.getMean()).isWithin(TOLERANCE).of(7.7);
+ assertThat(distributionData.getCount()).isEqualTo(10);
+ assertThat(distributionData.getMin()).isWithin(TOLERANCE).of(1.1);
+ assertThat(distributionData.getMax()).isWithin(TOLERANCE).of(9.9);
+ assertThat(distributionData.getSumOfSquaredDeviations()).isWithin(TOLERANCE).of(32.2);
+ assertThat(distributionData.getBucketCounts()).containsExactly(4L, 1L, 5L).inOrder();
+ }
+
+ @Test
+ public void testCreateDistributionDataWithExemplar() {
+ Exemplar exemplar1 = Exemplar.create(4, TIMESTAMP_2, ATTACHMENTS);
+ Exemplar exemplar2 = Exemplar.create(1, TIMESTAMP_1, ATTACHMENTS);
+ DistributionData distributionData =
+ DistributionData.create(
+ 7.7, 10, 1.1, 9.9, 32.2, Arrays.asList(4L, 1L), Arrays.asList(exemplar1, exemplar2));
+ assertThat(distributionData.getExemplars()).containsExactly(exemplar1, exemplar2).inOrder();
+ }
+
+ @Test
+ public void testExemplar() {
+ Exemplar exemplar = Exemplar.create(15.0, TIMESTAMP_1, ATTACHMENTS);
+ assertThat(exemplar.getValue()).isEqualTo(15.0);
+ assertThat(exemplar.getTimestamp()).isEqualTo(TIMESTAMP_1);
+ assertThat(exemplar.getAttachments()).isEqualTo(ATTACHMENTS);
+ }
+
+ @Test
+ public void testExemplar_PreventNullAttachments() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("attachments");
+ Exemplar.create(15, TIMESTAMP_1, null);
+ }
+
+ @Test
+ public void testExemplar_PreventNullAttachmentKey() {
+ Map<String, String> attachments = Collections.singletonMap(null, "value");
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("key of attachment");
+ Exemplar.create(15, TIMESTAMP_1, attachments);
+ }
+
+ @Test
+ public void testExemplar_PreventNullAttachmentValue() {
+ Map<String, String> attachments = Collections.singletonMap("key", null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("value of attachment");
+ Exemplar.create(15, TIMESTAMP_1, attachments);
+ }
+
+ @Test
+ public void preventNullBucketCountList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("bucketCounts");
+ DistributionData.create(1, 1, 1, 1, 0, null);
+ }
+
+ @Test
+ public void preventNullBucket() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("bucket");
+ DistributionData.create(1, 1, 1, 1, 0, Arrays.asList(0L, 1L, null));
+ }
+
+ @Test
+ public void preventNullExemplarList() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("exemplar list should not be null.");
+ DistributionData.create(1, 1, 1, 1, 0, Arrays.asList(0L, 1L, 1L), null);
+ }
+
+ @Test
+ public void preventNullExemplar() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("exemplar");
+ DistributionData.create(
+ 1, 1, 1, 1, 0, Arrays.asList(0L, 1L, 1L), Collections.<Exemplar>singletonList(null));
+ }
+
+ @Test
+ public void preventMinIsGreaterThanMax() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("max should be greater or equal to min.");
+ DistributionData.create(1, 1, 10, 1, 0, Arrays.asList(0L, 1L, 0L));
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(SumDataDouble.create(10.0), SumDataDouble.create(10.0))
+ .addEqualityGroup(SumDataDouble.create(20.0), SumDataDouble.create(20.0))
+ .addEqualityGroup(SumDataLong.create(20), SumDataLong.create(20))
+ .addEqualityGroup(CountData.create(40), CountData.create(40))
+ .addEqualityGroup(CountData.create(80), CountData.create(80))
+ .addEqualityGroup(
+ DistributionData.create(10, 10, 1, 1, 0, Arrays.asList(0L, 10L, 0L)),
+ DistributionData.create(10, 10, 1, 1, 0, Arrays.asList(0L, 10L, 0L)))
+ .addEqualityGroup(DistributionData.create(10, 10, 1, 1, 0, Arrays.asList(0L, 10L, 100L)))
+ .addEqualityGroup(DistributionData.create(110, 10, 1, 1, 0, Arrays.asList(0L, 10L, 0L)))
+ .addEqualityGroup(DistributionData.create(10, 110, 1, 1, 0, Arrays.asList(0L, 10L, 0L)))
+ .addEqualityGroup(DistributionData.create(10, 10, -1, 1, 0, Arrays.asList(0L, 10L, 0L)))
+ .addEqualityGroup(DistributionData.create(10, 10, 1, 5, 0, Arrays.asList(0L, 10L, 0L)))
+ .addEqualityGroup(DistributionData.create(10, 10, 1, 1, 55.5, Arrays.asList(0L, 10L, 0L)))
+ .addEqualityGroup(MeanData.create(5.0, 1), MeanData.create(5.0, 1))
+ .addEqualityGroup(MeanData.create(-5.0, 1), MeanData.create(-5.0, 1))
+ .addEqualityGroup(LastValueDataDouble.create(20.0), LastValueDataDouble.create(20.0))
+ .addEqualityGroup(LastValueDataLong.create(20), LastValueDataLong.create(20))
+ .testEquals();
+ }
+
+ @Test
+ public void testMatchAndGet() {
+ List<AggregationData> aggregations =
+ Arrays.asList(
+ SumDataDouble.create(10.0),
+ SumDataLong.create(100000000),
+ CountData.create(40),
+ DistributionData.create(1, 1, 1, 1, 0, Arrays.asList(0L, 10L, 0L)),
+ LastValueDataDouble.create(20.0),
+ LastValueDataLong.create(200000000L));
+
+ final List<Object> actual = new ArrayList<Object>();
+ for (AggregationData aggregation : aggregations) {
+ aggregation.match(
+ new Function<SumDataDouble, Void>() {
+ @Override
+ public Void apply(SumDataDouble arg) {
+ actual.add(arg.getSum());
+ return null;
+ }
+ },
+ new Function<SumDataLong, Void>() {
+ @Override
+ public Void apply(SumDataLong arg) {
+ actual.add(arg.getSum());
+ return null;
+ }
+ },
+ new Function<CountData, Void>() {
+ @Override
+ public Void apply(CountData arg) {
+ actual.add(arg.getCount());
+ return null;
+ }
+ },
+ new Function<DistributionData, Void>() {
+ @Override
+ public Void apply(DistributionData arg) {
+ actual.add(arg.getBucketCounts());
+ return null;
+ }
+ },
+ new Function<LastValueDataDouble, Void>() {
+ @Override
+ public Void apply(LastValueDataDouble arg) {
+ actual.add(arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<LastValueDataLong, Void>() {
+ @Override
+ public Void apply(LastValueDataLong arg) {
+ actual.add(arg.getLastValue());
+ return null;
+ }
+ },
+ Functions.<Void>throwIllegalArgumentException());
+ }
+
+ assertThat(actual)
+ .containsExactly(10.0, 100000000L, 40L, Arrays.asList(0L, 10L, 0L), 20.0, 200000000L)
+ .inOrder();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/stats/AggregationTest.java b/api/src/test/java/io/opencensus/stats/AggregationTest.java
new file mode 100644
index 00000000..cf337030
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/AggregationTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Functions;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+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.stats.Aggregation}. */
+@RunWith(JUnit4.class)
+public class AggregationTest {
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testCreateDistribution() {
+ BucketBoundaries bucketBoundaries = BucketBoundaries.create(Arrays.asList(0.1, 2.2, 33.3));
+ Distribution distribution = Distribution.create(bucketBoundaries);
+ assertThat(distribution.getBucketBoundaries()).isEqualTo(bucketBoundaries);
+ }
+
+ @Test
+ public void testNullBucketBoundaries() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("bucketBoundaries");
+ Distribution.create(null);
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(Sum.create(), Sum.create())
+ .addEqualityGroup(Count.create(), Count.create())
+ .addEqualityGroup(
+ Distribution.create(BucketBoundaries.create(Arrays.asList(-10.0, 1.0, 5.0))),
+ Distribution.create(BucketBoundaries.create(Arrays.asList(-10.0, 1.0, 5.0))))
+ .addEqualityGroup(
+ Distribution.create(BucketBoundaries.create(Arrays.asList(0.0, 1.0, 5.0))),
+ Distribution.create(BucketBoundaries.create(Arrays.asList(0.0, 1.0, 5.0))))
+ .addEqualityGroup(Mean.create(), Mean.create())
+ .addEqualityGroup(LastValue.create(), LastValue.create())
+ .testEquals();
+ }
+
+ @Test
+ public void testMatch() {
+ List<Aggregation> aggregations =
+ Arrays.asList(
+ Sum.create(),
+ Count.create(),
+ Mean.create(),
+ Distribution.create(BucketBoundaries.create(Arrays.asList(-10.0, 1.0, 5.0))),
+ LastValue.create());
+
+ List<String> actual = new ArrayList<String>();
+ for (Aggregation aggregation : aggregations) {
+ actual.add(
+ aggregation.match(
+ Functions.returnConstant("SUM"),
+ Functions.returnConstant("COUNT"),
+ Functions.returnConstant("DISTRIBUTION"),
+ Functions.returnConstant("LASTVALUE"),
+ Functions.returnConstant("UNKNOWN")));
+ }
+
+ assertThat(actual)
+ .isEqualTo(Arrays.asList("SUM", "COUNT", "UNKNOWN", "DISTRIBUTION", "LASTVALUE"));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/stats/BucketBoundariesTest.java b/api/src/test/java/io/opencensus/stats/BucketBoundariesTest.java
new file mode 100644
index 00000000..36f2edb4
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/BucketBoundariesTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+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.stats.BucketBoundaries}. */
+@RunWith(JUnit4.class)
+public class BucketBoundariesTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testConstructBoundaries() {
+ List<Double> buckets = Arrays.asList(0.0, 1.0, 2.0);
+ BucketBoundaries bucketBoundaries = BucketBoundaries.create(buckets);
+ assertThat(bucketBoundaries.getBoundaries()).isEqualTo(buckets);
+ }
+
+ @Test
+ public void testBoundariesDoesNotChangeWithOriginalList() {
+ List<Double> original = new ArrayList<Double>();
+ original.add(0.0);
+ original.add(1.0);
+ original.add(2.0);
+ BucketBoundaries bucketBoundaries = BucketBoundaries.create(original);
+ original.set(2, 3.0);
+ original.add(4.0);
+ List<Double> expected = Arrays.asList(0.0, 1.0, 2.0);
+ assertThat(bucketBoundaries.getBoundaries()).isNotEqualTo(original);
+ assertThat(bucketBoundaries.getBoundaries()).isEqualTo(expected);
+ }
+
+ @Test
+ public void testNullBoundaries() throws Exception {
+ thrown.expect(NullPointerException.class);
+ BucketBoundaries.create(null);
+ }
+
+ @Test
+ public void testUnsortedBoundaries() throws Exception {
+ List<Double> buckets = Arrays.asList(0.0, 1.0, 1.0);
+ thrown.expect(IllegalArgumentException.class);
+ BucketBoundaries.create(buckets);
+ }
+
+ @Test
+ public void testNoBoundaries() {
+ List<Double> buckets = Arrays.asList();
+ BucketBoundaries bucketBoundaries = BucketBoundaries.create(buckets);
+ assertThat(bucketBoundaries.getBoundaries()).isEqualTo(buckets);
+ }
+
+ @Test
+ public void testBucketBoundariesEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ BucketBoundaries.create(Arrays.asList(-1.0, 2.0)),
+ BucketBoundaries.create(Arrays.asList(-1.0, 2.0)))
+ .addEqualityGroup(BucketBoundaries.create(Arrays.asList(-1.0)))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/stats/MeasureTest.java b/api/src/test/java/io/opencensus/stats/MeasureTest.java
new file mode 100644
index 00000000..a9302425
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/MeasureTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+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 Measure}. */
+@RunWith(JUnit4.class)
+public final class MeasureTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testConstants() {
+ assertThat(Measure.NAME_MAX_LENGTH).isEqualTo(255);
+ }
+
+ @Test
+ public void preventTooLongMeasureName() {
+ char[] chars = new char[Measure.NAME_MAX_LENGTH + 1];
+ Arrays.fill(chars, 'a');
+ String longName = String.valueOf(chars);
+ thrown.expect(IllegalArgumentException.class);
+ Measure.MeasureDouble.create(longName, "description", "1");
+ }
+
+ @Test
+ public void preventNonPrintableMeasureName() {
+ thrown.expect(IllegalArgumentException.class);
+ Measure.MeasureDouble.create("\2", "description", "1");
+ }
+
+ @Test
+ public void testMeasureDoubleComponents() {
+ Measure measurement = Measure.MeasureDouble.create("Foo", "The description of Foo", "Mbit/s");
+ assertThat(measurement.getName()).isEqualTo("Foo");
+ assertThat(measurement.getDescription()).isEqualTo("The description of Foo");
+ assertThat(measurement.getUnit()).isEqualTo("Mbit/s");
+ }
+
+ @Test
+ public void testMeasureLongComponents() {
+ Measure measurement = Measure.MeasureLong.create("Bar", "The description of Bar", "1");
+ assertThat(measurement.getName()).isEqualTo("Bar");
+ assertThat(measurement.getDescription()).isEqualTo("The description of Bar");
+ assertThat(measurement.getUnit()).isEqualTo("1");
+ }
+
+ @Test
+ public void testMeasureDoubleEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ Measure.MeasureDouble.create("name", "description", "bit/s"),
+ Measure.MeasureDouble.create("name", "description", "bit/s"))
+ .addEqualityGroup(Measure.MeasureDouble.create("name", "description 2", "bit/s"))
+ .testEquals();
+ }
+
+ @Test
+ public void testMeasureLongEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ Measure.MeasureLong.create("name", "description", "bit/s"),
+ Measure.MeasureLong.create("name", "description", "bit/s"))
+ .addEqualityGroup(Measure.MeasureLong.create("name", "description 2", "bit/s"))
+ .testEquals();
+ }
+
+ @Test
+ public void testMatch() {
+ List<Measure> measures =
+ Arrays.asList(
+ MeasureDouble.create("measure1", "description", "1"),
+ MeasureLong.create("measure2", "description", "1"));
+ List<String> outputs = Lists.newArrayList();
+ for (Measure measure : measures) {
+ outputs.add(
+ measure.match(
+ new Function<MeasureDouble, String>() {
+ @Override
+ public String apply(MeasureDouble arg) {
+ return "double";
+ }
+ },
+ new Function<MeasureLong, String>() {
+ @Override
+ public String apply(MeasureLong arg) {
+ return "long";
+ }
+ },
+ Functions.<String>throwAssertionError()));
+ }
+ assertThat(outputs).containsExactly("double", "long").inOrder();
+ }
+
+ @Test
+ public void testMeasureDoubleIsNotEqualToMeasureLong() {
+ assertThat(Measure.MeasureDouble.create("name", "description", "bit/s"))
+ .isNotEqualTo(Measure.MeasureLong.create("name", "description", "bit/s"));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/stats/NoopStatsTest.java b/api/src/test/java/io/opencensus/stats/NoopStatsTest.java
new file mode 100644
index 00000000..4bae14a6
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/NoopStatsTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.stats.Measure.MeasureDouble;
+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.Iterator;
+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 NoopStats}. Tests for {@link NoopStats#newNoopViewManager} are in {@link
+ * NoopViewManagerTest}
+ */
+@RunWith(JUnit4.class)
+public final class NoopStatsTest {
+ private static final Tag TAG = Tag.create(TagKey.create("key"), TagValue.create("value"));
+ private static final MeasureDouble MEASURE =
+ Measure.MeasureDouble.create("my measure", "description", "s");
+
+ private final TagContext tagContext =
+ new TagContext() {
+
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return Collections.<Tag>singleton(TAG).iterator();
+ }
+ };
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void noopStatsComponent() {
+ assertThat(NoopStats.newNoopStatsComponent().getStatsRecorder())
+ .isSameAs(NoopStats.getNoopStatsRecorder());
+ assertThat(NoopStats.newNoopStatsComponent().getViewManager())
+ .isInstanceOf(NoopStats.newNoopViewManager().getClass());
+ }
+
+ @Test
+ public void noopStatsComponent_GetState() {
+ assertThat(NoopStats.newNoopStatsComponent().getState())
+ .isEqualTo(StatsCollectionState.DISABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void noopStatsComponent_SetState_IgnoresInput() {
+ StatsComponent noopStatsComponent = NoopStats.newNoopStatsComponent();
+ noopStatsComponent.setState(StatsCollectionState.ENABLED);
+ assertThat(noopStatsComponent.getState()).isEqualTo(StatsCollectionState.DISABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void noopStatsComponent_SetState_DisallowsNull() {
+ StatsComponent noopStatsComponent = NoopStats.newNoopStatsComponent();
+ thrown.expect(NullPointerException.class);
+ noopStatsComponent.setState(null);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void noopStatsComponent_DisallowsSetStateAfterGetState() {
+ StatsComponent noopStatsComponent = NoopStats.newNoopStatsComponent();
+ noopStatsComponent.setState(StatsCollectionState.DISABLED);
+ noopStatsComponent.getState();
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ noopStatsComponent.setState(StatsCollectionState.ENABLED);
+ }
+
+ @Test
+ public void noopStatsRecorder_PutAttachmentNullKey() {
+ MeasureMap measureMap = NoopStats.getNoopStatsRecorder().newMeasureMap();
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("key");
+ measureMap.putAttachment(null, "value");
+ }
+
+ @Test
+ public void noopStatsRecorder_PutAttachmentNullValue() {
+ MeasureMap measureMap = NoopStats.getNoopStatsRecorder().newMeasureMap();
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("value");
+ measureMap.putAttachment("key", null);
+ }
+
+ // The NoopStatsRecorder should do nothing, so this test just checks that record doesn't throw an
+ // exception.
+ @Test
+ public void noopStatsRecorder_Record() {
+ NoopStats.getNoopStatsRecorder().newMeasureMap().put(MEASURE, 5).record(tagContext);
+ }
+
+ // The NoopStatsRecorder should do nothing, so this test just checks that record doesn't throw an
+ // exception.
+ @Test
+ public void noopStatsRecorder_RecordWithCurrentContext() {
+ NoopStats.getNoopStatsRecorder().newMeasureMap().put(MEASURE, 6).record();
+ }
+
+ @Test
+ public void noopStatsRecorder_Record_DisallowNullTagContext() {
+ MeasureMap measureMap = NoopStats.getNoopStatsRecorder().newMeasureMap();
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("tags");
+ measureMap.record(null);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/stats/NoopViewManagerTest.java b/api/src/test/java/io/opencensus/stats/NoopViewManagerTest.java
new file mode 100644
index 00000000..44c7626f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/NoopViewManagerTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.Measure.MeasureDouble;
+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.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import java.util.Arrays;
+import java.util.Set;
+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 NoopStats#newNoopViewManager}. */
+@RunWith(JUnit4.class)
+public final class NoopViewManagerTest {
+ private static final MeasureDouble MEASURE =
+ Measure.MeasureDouble.create("my measure", "description", "s");
+ private static final TagKey KEY = TagKey.create("KEY");
+ private static final Name VIEW_NAME = Name.create("my view");
+ private static final String VIEW_DESCRIPTION = "view description";
+ private static final Sum AGGREGATION = Sum.create();
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final Duration TEN_SECONDS = Duration.create(10, 0);
+ private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void noopViewManager_RegisterView_DisallowRegisteringDifferentViewWithSameName() {
+ final View view1 =
+ View.create(
+ VIEW_NAME, "description 1", MEASURE, AGGREGATION, Arrays.asList(KEY), CUMULATIVE);
+ final View view2 =
+ View.create(
+ VIEW_NAME, "description 2", MEASURE, AGGREGATION, Arrays.asList(KEY), CUMULATIVE);
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ viewManager.registerView(view1);
+
+ try {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("A different view with the same name already exists.");
+ viewManager.registerView(view2);
+ } finally {
+ assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(view1);
+ }
+ }
+
+ @Test
+ public void noopViewManager_RegisterView_AllowRegisteringSameViewTwice() {
+ View view =
+ View.create(
+ VIEW_NAME, VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY), CUMULATIVE);
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ viewManager.registerView(view);
+ viewManager.registerView(view);
+ }
+
+ @Test
+ public void noopViewManager_RegisterView_DisallowNull() {
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ thrown.expect(NullPointerException.class);
+ viewManager.registerView(null);
+ }
+
+ @Test
+ public void noopViewManager_GetView_GettingNonExistentViewReturnsNull() {
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ assertThat(viewManager.getView(VIEW_NAME)).isNull();
+ }
+
+ @Test
+ public void noopViewManager_GetView_Cumulative() {
+ View view =
+ View.create(
+ VIEW_NAME, VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY), CUMULATIVE);
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ viewManager.registerView(view);
+
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertThat(viewData.getView()).isEqualTo(view);
+ assertThat(viewData.getAggregationMap()).isEmpty();
+ assertThat(viewData.getStart()).isEqualTo(Timestamp.create(0, 0));
+ assertThat(viewData.getEnd()).isEqualTo(Timestamp.create(0, 0));
+ assertThat(viewData.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(0, 0), Timestamp.create(0, 0)));
+ }
+
+ @Test
+ public void noopViewManager_GetView_Interval() {
+ View view =
+ View.create(
+ VIEW_NAME, VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY), INTERVAL);
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ viewManager.registerView(view);
+
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertThat(viewData.getView()).isEqualTo(view);
+ assertThat(viewData.getAggregationMap()).isEmpty();
+ assertThat(viewData.getWindowData()).isEqualTo(IntervalData.create(Timestamp.create(0, 0)));
+ }
+
+ @Test
+ public void noopViewManager_GetView_DisallowNull() {
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ thrown.expect(NullPointerException.class);
+ viewManager.getView(null);
+ }
+
+ @Test
+ public void getAllExportedViews() {
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ assertThat(viewManager.getAllExportedViews()).isEmpty();
+ View cumulativeView1 =
+ View.create(
+ View.Name.create("View 1"),
+ VIEW_DESCRIPTION,
+ MEASURE,
+ AGGREGATION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ View cumulativeView2 =
+ View.create(
+ View.Name.create("View 2"),
+ VIEW_DESCRIPTION,
+ MEASURE,
+ AGGREGATION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ View intervalView =
+ View.create(
+ View.Name.create("View 3"),
+ VIEW_DESCRIPTION,
+ MEASURE,
+ AGGREGATION,
+ Arrays.asList(KEY),
+ INTERVAL);
+ viewManager.registerView(cumulativeView1);
+ viewManager.registerView(cumulativeView2);
+ viewManager.registerView(intervalView);
+
+ // Only cumulative views should be exported.
+ assertThat(viewManager.getAllExportedViews()).containsExactly(cumulativeView1, cumulativeView2);
+ }
+
+ @Test
+ public void getAllExportedViews_ResultIsUnmodifiable() {
+ ViewManager viewManager = NoopStats.newNoopViewManager();
+ View view1 =
+ View.create(
+ View.Name.create("View 1"), VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY));
+ viewManager.registerView(view1);
+ Set<View> exported = viewManager.getAllExportedViews();
+
+ View view2 =
+ View.create(
+ View.Name.create("View 2"), VIEW_DESCRIPTION, MEASURE, AGGREGATION, Arrays.asList(KEY));
+ thrown.expect(UnsupportedOperationException.class);
+ exported.add(view2);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/stats/StatsTest.java b/api/src/test/java/io/opencensus/stats/StatsTest.java
new file mode 100644
index 00000000..4219173a
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/StatsTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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 Stats}. */
+@RunWith(JUnit4.class)
+public final class StatsTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void loadStatsManager_UsesProvidedClassLoader() {
+ final RuntimeException toThrow = new RuntimeException("UseClassLoader");
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("UseClassLoader");
+ Stats.loadStatsComponent(
+ new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) {
+ throw toThrow;
+ }
+ });
+ }
+
+ @Test
+ public void loadStatsManager_IgnoresMissingClasses() {
+ ClassLoader classLoader =
+ new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) throws ClassNotFoundException {
+ throw new ClassNotFoundException();
+ }
+ };
+
+ assertThat(Stats.loadStatsComponent(classLoader).getClass().getName())
+ .isEqualTo("io.opencensus.stats.NoopStats$NoopStatsComponent");
+ }
+
+ @Test
+ public void defaultValues() {
+ assertThat(Stats.getStatsRecorder()).isEqualTo(NoopStats.getNoopStatsRecorder());
+ assertThat(Stats.getViewManager()).isInstanceOf(NoopStats.newNoopViewManager().getClass());
+ }
+
+ @Test
+ public void getState() {
+ assertThat(Stats.getState()).isEqualTo(StatsCollectionState.DISABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void setState_IgnoresInput() {
+ Stats.setState(StatsCollectionState.ENABLED);
+ assertThat(Stats.getState()).isEqualTo(StatsCollectionState.DISABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void setState_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("state");
+ Stats.setState(null);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/stats/ViewDataTest.java b/api/src/test/java/io/opencensus/stats/ViewDataTest.java
new file mode 100644
index 00000000..0120ffea
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/ViewDataTest.java
@@ -0,0 +1,301 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.View.AggregationWindow;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.ViewData.AggregationWindowData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+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 class {@link ViewData}. */
+@RunWith(JUnit4.class)
+public final class ViewDataTest {
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testCumulativeViewData() {
+ View view = View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, CUMULATIVE);
+ Timestamp start = Timestamp.fromMillis(1000);
+ Timestamp end = Timestamp.fromMillis(2000);
+ AggregationWindowData windowData = CumulativeData.create(start, end);
+ ViewData viewData = ViewData.create(view, ENTRIES, windowData);
+ assertThat(viewData.getView()).isEqualTo(view);
+ assertThat(viewData.getAggregationMap()).isEqualTo(ENTRIES);
+ assertThat(viewData.getWindowData()).isEqualTo(windowData);
+ }
+
+ @Test
+ public void testIntervalViewData() {
+ View view =
+ View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, INTERVAL_HOUR);
+ Timestamp end = Timestamp.fromMillis(2000);
+ AggregationWindowData windowData = IntervalData.create(end);
+ ViewData viewData = ViewData.create(view, ENTRIES, windowData);
+ assertThat(viewData.getView()).isEqualTo(view);
+ assertThat(viewData.getAggregationMap()).isEqualTo(ENTRIES);
+ assertThat(viewData.getWindowData()).isEqualTo(windowData);
+ }
+
+ @Test
+ public void testViewDataEquals() {
+ View cumulativeView =
+ View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, CUMULATIVE);
+ View intervalView =
+ View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, INTERVAL_HOUR);
+
+ new EqualsTester()
+ .addEqualityGroup(
+ ViewData.create(
+ cumulativeView,
+ ENTRIES,
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000))),
+ ViewData.create(
+ cumulativeView,
+ ENTRIES,
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000))))
+ .addEqualityGroup(
+ ViewData.create(
+ cumulativeView,
+ ENTRIES,
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(3000))))
+ .addEqualityGroup(
+ ViewData.create(intervalView, ENTRIES, IntervalData.create(Timestamp.fromMillis(2000))),
+ ViewData.create(intervalView, ENTRIES, IntervalData.create(Timestamp.fromMillis(2000))))
+ .addEqualityGroup(
+ ViewData.create(
+ intervalView,
+ Collections.<List<TagValue>, AggregationData>emptyMap(),
+ IntervalData.create(Timestamp.fromMillis(2000))))
+ .testEquals();
+ }
+
+ @Test
+ public void testAggregationWindowDataMatch() {
+ final Timestamp start = Timestamp.fromMillis(1000);
+ final Timestamp end = Timestamp.fromMillis(2000);
+ final AggregationWindowData windowData1 = CumulativeData.create(start, end);
+ final AggregationWindowData windowData2 = IntervalData.create(end);
+ windowData1.match(
+ new Function<CumulativeData, Void>() {
+ @Override
+ public Void apply(CumulativeData windowData) {
+ assertThat(windowData.getStart()).isEqualTo(start);
+ assertThat(windowData.getEnd()).isEqualTo(end);
+ return null;
+ }
+ },
+ new Function<IntervalData, Void>() {
+ @Override
+ public Void apply(IntervalData windowData) {
+ fail("CumulativeData expected.");
+ return null;
+ }
+ },
+ Functions.<Void>throwIllegalArgumentException());
+ windowData2.match(
+ new Function<CumulativeData, Void>() {
+ @Override
+ public Void apply(CumulativeData windowData) {
+ fail("IntervalData expected.");
+ return null;
+ }
+ },
+ new Function<IntervalData, Void>() {
+ @Override
+ public Void apply(IntervalData windowData) {
+ assertThat(windowData.getEnd()).isEqualTo(end);
+ return null;
+ }
+ },
+ Functions.<Void>throwIllegalArgumentException());
+ }
+
+ @Test
+ public void preventWindowAndAggregationWindowDataMismatch() {
+ CumulativeData cumulativeData =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(
+ "AggregationWindow and AggregationWindowData types mismatch. "
+ + "AggregationWindow: "
+ + INTERVAL_HOUR.getClass().getSimpleName()
+ + " AggregationWindowData: "
+ + cumulativeData.getClass().getSimpleName());
+ ViewData.create(
+ View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, INTERVAL_HOUR),
+ ENTRIES,
+ cumulativeData);
+ }
+
+ @Test
+ public void preventWindowAndAggregationWindowDataMismatch2() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("AggregationWindow and AggregationWindowData types mismatch. ");
+ ViewData.create(
+ View.create(NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, TAG_KEYS, CUMULATIVE),
+ ENTRIES,
+ IntervalData.create(Timestamp.fromMillis(1000)));
+ }
+
+ @Test
+ public void preventStartTimeLaterThanEndTime() {
+ thrown.expect(IllegalArgumentException.class);
+ CumulativeData.create(Timestamp.fromMillis(3000), Timestamp.fromMillis(2000));
+ }
+
+ @Test
+ public void preventAggregationAndAggregationDataMismatch_SumDouble_SumLong() {
+ aggregationAndAggregationDataMismatch(
+ createView(Sum.create(), MEASURE_DOUBLE),
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ Arrays.asList(V1, V2), SumDataLong.create(100)));
+ }
+
+ @Test
+ public void preventAggregationAndAggregationDataMismatch_SumLong_SumDouble() {
+ aggregationAndAggregationDataMismatch(
+ createView(Sum.create(), MEASURE_LONG),
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ Arrays.asList(V1, V2), SumDataDouble.create(100)));
+ }
+
+ @Test
+ public void preventAggregationAndAggregationDataMismatch_Count_Distribution() {
+ aggregationAndAggregationDataMismatch(createView(Count.create()), ENTRIES);
+ }
+
+ @Test
+ public void preventAggregationAndAggregationDataMismatch_Mean_Distribution() {
+ aggregationAndAggregationDataMismatch(createView(Mean.create()), ENTRIES);
+ }
+
+ @Test
+ public void preventAggregationAndAggregationDataMismatch_Distribution_Count() {
+ aggregationAndAggregationDataMismatch(
+ createView(DISTRIBUTION), ImmutableMap.of(Arrays.asList(V10, V20), CountData.create(100)));
+ }
+
+ @Test
+ public void preventAggregationAndAggregationDataMismatch_LastValueDouble_LastValueLong() {
+ aggregationAndAggregationDataMismatch(
+ createView(LastValue.create(), MEASURE_DOUBLE),
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ Arrays.asList(V1, V2), LastValueDataLong.create(100)));
+ }
+
+ @Test
+ public void preventAggregationAndAggregationDataMismatch_LastValueLong_LastValueDouble() {
+ aggregationAndAggregationDataMismatch(
+ createView(LastValue.create(), MEASURE_LONG),
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ Arrays.asList(V1, V2), LastValueDataDouble.create(100)));
+ }
+
+ private static View createView(Aggregation aggregation) {
+ return createView(aggregation, MEASURE_DOUBLE);
+ }
+
+ private static View createView(Aggregation aggregation, Measure measure) {
+ return View.create(NAME, DESCRIPTION, measure, aggregation, TAG_KEYS, CUMULATIVE);
+ }
+
+ private void aggregationAndAggregationDataMismatch(
+ View view, Map<List<TagValue>, ? extends AggregationData> entries) {
+ CumulativeData cumulativeData =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ Aggregation aggregation = view.getAggregation();
+ AggregationData aggregationData = entries.values().iterator().next();
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(
+ "Aggregation and AggregationData types mismatch. "
+ + "Aggregation: "
+ + aggregation.getClass().getSimpleName()
+ + " AggregationData: "
+ + aggregationData.getClass().getSimpleName());
+ ViewData.create(view, entries, cumulativeData);
+ }
+
+ // tag keys
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = TagKey.create("k2");
+ private static final List<TagKey> TAG_KEYS = Arrays.asList(K1, K2);
+
+ // tag values
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+ private static final TagValue V10 = TagValue.create("v10");
+ private static final TagValue V20 = TagValue.create("v20");
+
+ private static final AggregationWindow CUMULATIVE = Cumulative.create();
+ private static final AggregationWindow INTERVAL_HOUR = Interval.create(Duration.create(3600, 0));
+
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(10.0, 20.0, 30.0, 40.0));
+
+ private static final Aggregation DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+
+ private static final ImmutableMap<List<TagValue>, DistributionData> ENTRIES =
+ ImmutableMap.of(
+ Arrays.asList(V1, V2),
+ DistributionData.create(1, 1, 1, 1, 0, Arrays.asList(0L, 1L, 0L)),
+ Arrays.asList(V10, V20),
+ DistributionData.create(-5, 6, -20, 5, 100.1, Arrays.asList(5L, 0L, 1L)));
+
+ // name
+ private static final View.Name NAME = View.Name.create("test-view");
+ // description
+ private static final String DESCRIPTION = "test-view-descriptor description";
+ // measure
+ private static final Measure MEASURE_DOUBLE =
+ Measure.MeasureDouble.create("measure1", "measure description", "1");
+ private static final Measure MEASURE_LONG =
+ Measure.MeasureLong.create("measure2", "measure description", "1");
+}
diff --git a/api/src/test/java/io/opencensus/stats/ViewTest.java b/api/src/test/java/io/opencensus/stats/ViewTest.java
new file mode 100644
index 00000000..afba1bc0
--- /dev/null
+++ b/api/src/test/java/io/opencensus/stats/ViewTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.tags.TagKey;
+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 View}. */
+@RunWith(JUnit4.class)
+public final class ViewTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testConstants() {
+ assertThat(View.NAME_MAX_LENGTH).isEqualTo(255);
+ }
+
+ @Test
+ public void sortTagKeys() {
+ final View view =
+ View.create(
+ NAME,
+ DESCRIPTION,
+ MEASURE,
+ MEAN,
+ Arrays.asList(
+ TagKey.create("ab"), TagKey.create("a"), TagKey.create("A"), TagKey.create("b")));
+ assertThat(view.getColumns())
+ .containsExactly(
+ TagKey.create("A"), TagKey.create("a"), TagKey.create("ab"), TagKey.create("b"))
+ .inOrder();
+ }
+
+ @Test
+ public void testDistributionView() {
+ final View view = View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS);
+ assertThat(view.getName()).isEqualTo(NAME);
+ assertThat(view.getDescription()).isEqualTo(DESCRIPTION);
+ assertThat(view.getMeasure().getName()).isEqualTo(MEASURE.getName());
+ assertThat(view.getAggregation()).isEqualTo(MEAN);
+ assertThat(view.getColumns()).containsExactly(BAR, FOO).inOrder();
+ assertThat(view.getWindow()).isEqualTo(Cumulative.create());
+ }
+
+ @Test
+ public void testIntervalView() {
+ final View view = View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Interval.create(MINUTE));
+ assertThat(view.getName()).isEqualTo(NAME);
+ assertThat(view.getDescription()).isEqualTo(DESCRIPTION);
+ assertThat(view.getMeasure().getName()).isEqualTo(MEASURE.getName());
+ assertThat(view.getAggregation()).isEqualTo(MEAN);
+ assertThat(view.getColumns()).containsExactly(BAR, FOO).inOrder();
+ assertThat(view.getWindow()).isEqualTo(Interval.create(MINUTE));
+ }
+
+ @Test
+ public void testViewEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS),
+ View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Cumulative.create()))
+ .addEqualityGroup(
+ View.create(NAME, DESCRIPTION + 2, MEASURE, MEAN, KEYS, Cumulative.create()))
+ .addEqualityGroup(
+ View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Interval.create(MINUTE)),
+ View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Interval.create(MINUTE)))
+ .addEqualityGroup(
+ View.create(NAME, DESCRIPTION, MEASURE, MEAN, KEYS, Interval.create(TWO_MINUTES)))
+ .testEquals();
+ }
+
+ @Test
+ public void preventDuplicateColumns() {
+ TagKey key1 = TagKey.create("duplicate");
+ TagKey key2 = TagKey.create("duplicate");
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Columns have duplicate.");
+ View.create(NAME, DESCRIPTION, MEASURE, MEAN, Arrays.asList(key1, key2));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void preventNullViewName() {
+ View.create(null, DESCRIPTION, MEASURE, MEAN, KEYS);
+ }
+
+ @Test
+ public void preventTooLongViewName() {
+ char[] chars = new char[View.NAME_MAX_LENGTH + 1];
+ Arrays.fill(chars, 'a');
+ String longName = String.valueOf(chars);
+ thrown.expect(IllegalArgumentException.class);
+ View.Name.create(longName);
+ }
+
+ @Test
+ public void preventNonPrintableViewName() {
+ thrown.expect(IllegalArgumentException.class);
+ View.Name.create("\2");
+ }
+
+ @Test
+ public void testViewName() {
+ assertThat(View.Name.create("my name").asString()).isEqualTo("my name");
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void preventNullNameString() {
+ View.Name.create(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void preventNegativeIntervalDuration() {
+ Interval.create(NEG_TEN_SECONDS);
+ }
+
+ @Test
+ public void testViewNameEquals() {
+ new EqualsTester()
+ .addEqualityGroup(View.Name.create("view-1"), View.Name.create("view-1"))
+ .addEqualityGroup(View.Name.create("view-2"))
+ .testEquals();
+ }
+
+ private static final View.Name NAME = View.Name.create("test-view-name");
+ private static final String DESCRIPTION = "test-view-name description";
+ private static final Measure MEASURE =
+ Measure.MeasureDouble.create("measure", "measure description", "1");
+ private static final TagKey FOO = TagKey.create("foo");
+ private static final TagKey BAR = TagKey.create("bar");
+ private static final List<TagKey> KEYS = Collections.unmodifiableList(Arrays.asList(FOO, BAR));
+ private static final Mean MEAN = Mean.create();
+ private static final Duration MINUTE = Duration.create(60, 0);
+ private static final Duration TWO_MINUTES = Duration.create(120, 0);
+ private static final Duration NEG_TEN_SECONDS = Duration.create(-10, 0);
+}
diff --git a/api/src/test/java/io/opencensus/tags/InternalUtilsTest.java b/api/src/test/java/io/opencensus/tags/InternalUtilsTest.java
new file mode 100644
index 00000000..65482de1
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/InternalUtilsTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import java.util.Iterator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InternalUtils}. */
+@RunWith(JUnit4.class)
+public final class InternalUtilsTest {
+
+ @Test
+ public void getTags() {
+ final Iterator<Tag> iterator =
+ Lists.<Tag>newArrayList(Tag.create(TagKey.create("k"), TagValue.create("v"))).iterator();
+ TagContext ctx =
+ new TagContext() {
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return iterator;
+ }
+ };
+ assertThat(InternalUtils.getTags(ctx)).isSameAs(iterator);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/NoopTagsTest.java b/api/src/test/java/io/opencensus/tags/NoopTagsTest.java
new file mode 100644
index 00000000..db07520e
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/NoopTagsTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.opencensus.internal.NoopScope;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+import java.util.Arrays;
+import java.util.Iterator;
+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 NoopTags}. */
+@RunWith(JUnit4.class)
+public final class NoopTagsTest {
+ private static final TagKey KEY = TagKey.create("key");
+ private static final TagValue VALUE = TagValue.create("value");
+
+ private static final TagContext TAG_CONTEXT =
+ new TagContext() {
+
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return Arrays.<Tag>asList(Tag.create(KEY, VALUE)).iterator();
+ }
+ };
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void noopTagsComponent() {
+ assertThat(NoopTags.newNoopTagsComponent().getTagger()).isSameAs(NoopTags.getNoopTagger());
+ assertThat(NoopTags.newNoopTagsComponent().getTagPropagationComponent())
+ .isSameAs(NoopTags.getNoopTagPropagationComponent());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void noopTagsComponent_SetState_DisallowsNull() {
+ TagsComponent noopTagsComponent = NoopTags.newNoopTagsComponent();
+ thrown.expect(NullPointerException.class);
+ noopTagsComponent.setState(null);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void preventSettingStateAfterGettingState_DifferentState() {
+ TagsComponent noopTagsComponent = NoopTags.newNoopTagsComponent();
+ noopTagsComponent.setState(TaggingState.DISABLED);
+ noopTagsComponent.getState();
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ noopTagsComponent.setState(TaggingState.ENABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void preventSettingStateAfterGettingState_SameState() {
+ TagsComponent noopTagsComponent = NoopTags.newNoopTagsComponent();
+ noopTagsComponent.setState(TaggingState.DISABLED);
+ noopTagsComponent.getState();
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ noopTagsComponent.setState(TaggingState.DISABLED);
+ }
+
+ @Test
+ public void noopTagger() {
+ Tagger noopTagger = NoopTags.getNoopTagger();
+ assertThat(noopTagger.empty()).isSameAs(NoopTags.getNoopTagContext());
+ assertThat(noopTagger.getCurrentTagContext()).isSameAs(NoopTags.getNoopTagContext());
+ assertThat(noopTagger.emptyBuilder()).isSameAs(NoopTags.getNoopTagContextBuilder());
+ assertThat(noopTagger.toBuilder(TAG_CONTEXT)).isSameAs(NoopTags.getNoopTagContextBuilder());
+ assertThat(noopTagger.currentBuilder()).isSameAs(NoopTags.getNoopTagContextBuilder());
+ assertThat(noopTagger.withTagContext(TAG_CONTEXT)).isSameAs(NoopScope.getInstance());
+ }
+
+ @Test
+ public void noopTagger_ToBuilder_DisallowsNull() {
+ Tagger noopTagger = NoopTags.getNoopTagger();
+ thrown.expect(NullPointerException.class);
+ noopTagger.toBuilder(null);
+ }
+
+ @Test
+ public void noopTagger_WithTagContext_DisallowsNull() {
+ Tagger noopTagger = NoopTags.getNoopTagger();
+ thrown.expect(NullPointerException.class);
+ noopTagger.withTagContext(null);
+ }
+
+ @Test
+ public void noopTagContextBuilder() {
+ assertThat(NoopTags.getNoopTagContextBuilder().build()).isSameAs(NoopTags.getNoopTagContext());
+ assertThat(NoopTags.getNoopTagContextBuilder().put(KEY, VALUE).build())
+ .isSameAs(NoopTags.getNoopTagContext());
+ assertThat(NoopTags.getNoopTagContextBuilder().buildScoped()).isSameAs(NoopScope.getInstance());
+ assertThat(NoopTags.getNoopTagContextBuilder().put(KEY, VALUE).buildScoped())
+ .isSameAs(NoopScope.getInstance());
+ }
+
+ @Test
+ public void noopTagContextBuilder_Put_DisallowsNullKey() {
+ TagContextBuilder noopBuilder = NoopTags.getNoopTagContextBuilder();
+ thrown.expect(NullPointerException.class);
+ noopBuilder.put(null, VALUE);
+ }
+
+ @Test
+ public void noopTagContextBuilder_Put_DisallowsNullValue() {
+ TagContextBuilder noopBuilder = NoopTags.getNoopTagContextBuilder();
+ thrown.expect(NullPointerException.class);
+ noopBuilder.put(KEY, null);
+ }
+
+ @Test
+ public void noopTagContextBuilder_Remove_DisallowsNullKey() {
+ TagContextBuilder noopBuilder = NoopTags.getNoopTagContextBuilder();
+ thrown.expect(NullPointerException.class);
+ noopBuilder.remove(null);
+ }
+
+ @Test
+ public void noopTagContext() {
+ assertThat(Lists.newArrayList(NoopTags.getNoopTagContext().getIterator())).isEmpty();
+ }
+
+ @Test
+ public void noopTagPropagationComponent() {
+ assertThat(NoopTags.getNoopTagPropagationComponent().getBinarySerializer())
+ .isSameAs(NoopTags.getNoopTagContextBinarySerializer());
+ }
+
+ @Test
+ public void noopTagContextBinarySerializer()
+ throws TagContextDeserializationException, TagContextSerializationException {
+ assertThat(NoopTags.getNoopTagContextBinarySerializer().toByteArray(TAG_CONTEXT))
+ .isEqualTo(new byte[0]);
+ assertThat(NoopTags.getNoopTagContextBinarySerializer().fromByteArray(new byte[5]))
+ .isEqualTo(NoopTags.getNoopTagContext());
+ }
+
+ @Test
+ public void noopTagContextBinarySerializer_ToByteArray_DisallowsNull()
+ throws TagContextSerializationException {
+ TagContextBinarySerializer noopSerializer = NoopTags.getNoopTagContextBinarySerializer();
+ thrown.expect(NullPointerException.class);
+ noopSerializer.toByteArray(null);
+ }
+
+ @Test
+ public void noopTagContextBinarySerializer_FromByteArray_DisallowsNull()
+ throws TagContextDeserializationException {
+ TagContextBinarySerializer noopSerializer = NoopTags.getNoopTagContextBinarySerializer();
+ thrown.expect(NullPointerException.class);
+ noopSerializer.fromByteArray(null);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagContextTest.java b/api/src/test/java/io/opencensus/tags/TagContextTest.java
new file mode 100644
index 00000000..025c7bae
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagContextTest.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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TagContext}. */
+@RunWith(JUnit4.class)
+public final class TagContextTest {
+ private static final Tag TAG1 = Tag.create(TagKey.create("key"), TagValue.create("val"));
+ private static final Tag TAG2 = Tag.create(TagKey.create("key2"), TagValue.create("val"));
+
+ @Test
+ public void equals_IgnoresTagOrderAndTagContextClass() {
+ new EqualsTester()
+ .addEqualityGroup(
+ new SimpleTagContext(TAG1, TAG2),
+ new SimpleTagContext(TAG1, TAG2),
+ new SimpleTagContext(TAG2, TAG1),
+ new TagContext() {
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return Lists.newArrayList(TAG1, TAG2).iterator();
+ }
+ })
+ .testEquals();
+ }
+
+ @Test
+ public void equals_HandlesNullIterator() {
+ new EqualsTester()
+ .addEqualityGroup(
+ new SimpleTagContext((List<Tag>) null),
+ new SimpleTagContext((List<Tag>) null),
+ new SimpleTagContext())
+ .testEquals();
+ }
+
+ @Test
+ public void equals_DoesNotIgnoreNullTags() {
+ new EqualsTester()
+ .addEqualityGroup(new SimpleTagContext(TAG1))
+ .addEqualityGroup(new SimpleTagContext(TAG1, null), new SimpleTagContext(null, TAG1))
+ .addEqualityGroup(new SimpleTagContext(TAG1, null, null))
+ .testEquals();
+ }
+
+ @Test
+ public void equals_DoesNotIgnoreDuplicateTags() {
+ new EqualsTester()
+ .addEqualityGroup(new SimpleTagContext(TAG1))
+ .addEqualityGroup(new SimpleTagContext(TAG1, TAG1))
+ .testEquals();
+ }
+
+ @Test
+ public void testToString() {
+ assertThat(new SimpleTagContext().toString()).isEqualTo("TagContext");
+ assertThat(new SimpleTagContext(TAG1, TAG2).toString()).isEqualTo("TagContext");
+ }
+
+ private static final class SimpleTagContext extends TagContext {
+ @Nullable private final List<Tag> tags;
+
+ SimpleTagContext(Tag... tags) {
+ this(Lists.newArrayList(tags));
+ }
+
+ SimpleTagContext(List<Tag> tags) {
+ this.tags = tags == null ? null : Collections.unmodifiableList(Lists.newArrayList(tags));
+ }
+
+ @Override
+ @Nullable
+ protected Iterator<Tag> getIterator() {
+ return tags == null ? null : tags.iterator();
+ }
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagKeyTest.java b/api/src/test/java/io/opencensus/tags/TagKeyTest.java
new file mode 100644
index 00000000..48cf9fd4
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagKeyTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+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 TagKey}. */
+@RunWith(JUnit4.class)
+public final class TagKeyTest {
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testMaxLength() {
+ assertThat(TagKey.MAX_LENGTH).isEqualTo(255);
+ }
+
+ @Test
+ public void testGetName() {
+ assertThat(TagKey.create("foo").getName()).isEqualTo("foo");
+ }
+
+ @Test
+ public void create_AllowTagKeyNameWithMaxLength() {
+ char[] chars = new char[TagKey.MAX_LENGTH];
+ Arrays.fill(chars, 'k');
+ String key = new String(chars);
+ assertThat(TagKey.create(key).getName()).isEqualTo(key);
+ }
+
+ @Test
+ public void create_DisallowTagKeyNameOverMaxLength() {
+ char[] chars = new char[TagKey.MAX_LENGTH + 1];
+ Arrays.fill(chars, 'k');
+ String key = new String(chars);
+ thrown.expect(IllegalArgumentException.class);
+ TagKey.create(key);
+ }
+
+ @Test
+ public void create_DisallowUnprintableChars() {
+ thrown.expect(IllegalArgumentException.class);
+ TagKey.create("\2ab\3cd");
+ }
+
+ @Test
+ public void createString_DisallowEmpty() {
+ thrown.expect(IllegalArgumentException.class);
+ TagKey.create("");
+ }
+
+ @Test
+ public void testTagKeyEquals() {
+ new EqualsTester()
+ .addEqualityGroup(TagKey.create("foo"), TagKey.create("foo"))
+ .addEqualityGroup(TagKey.create("bar"))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagTest.java b/api/src/test/java/io/opencensus/tags/TagTest.java
new file mode 100644
index 00000000..3c899e65
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link Tag}. */
+@RunWith(JUnit4.class)
+public final class TagTest {
+
+ @Test
+ public void testGetKey() {
+ assertThat(Tag.create(TagKey.create("k"), TagValue.create("v")).getKey())
+ .isEqualTo(TagKey.create("k"));
+ }
+
+ @Test
+ public void testTagEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ Tag.create(TagKey.create("Key"), TagValue.create("foo")),
+ Tag.create(TagKey.create("Key"), TagValue.create("foo")))
+ .addEqualityGroup(Tag.create(TagKey.create("Key"), TagValue.create("bar")))
+ .addEqualityGroup(Tag.create(TagKey.create("Key2"), TagValue.create("foo")))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagValueTest.java b/api/src/test/java/io/opencensus/tags/TagValueTest.java
new file mode 100644
index 00000000..9aa42c8c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagValueTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+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 TagValue}. */
+@RunWith(JUnit4.class)
+public final class TagValueTest {
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testMaxLength() {
+ assertThat(TagValue.MAX_LENGTH).isEqualTo(255);
+ }
+
+ @Test
+ public void testAsString() {
+ assertThat(TagValue.create("foo").asString()).isEqualTo("foo");
+ }
+
+ @Test
+ public void create_AllowTagValueWithMaxLength() {
+ char[] chars = new char[TagValue.MAX_LENGTH];
+ Arrays.fill(chars, 'v');
+ String value = new String(chars);
+ assertThat(TagValue.create(value).asString()).isEqualTo(value);
+ }
+
+ @Test
+ public void create_DisallowTagValueOverMaxLength() {
+ char[] chars = new char[TagValue.MAX_LENGTH + 1];
+ Arrays.fill(chars, 'v');
+ String value = new String(chars);
+ thrown.expect(IllegalArgumentException.class);
+ TagValue.create(value);
+ }
+
+ @Test
+ public void disallowTagValueWithUnprintableChars() {
+ String value = "\2ab\3cd";
+ thrown.expect(IllegalArgumentException.class);
+ TagValue.create(value);
+ }
+
+ @Test
+ public void testTagValueEquals() {
+ new EqualsTester()
+ .addEqualityGroup(TagValue.create("foo"), TagValue.create("foo"))
+ .addEqualityGroup(TagValue.create("bar"))
+ .testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/TagsTest.java b/api/src/test/java/io/opencensus/tags/TagsTest.java
new file mode 100644
index 00000000..dee517b6
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/TagsTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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 Tags}. */
+@RunWith(JUnit4.class)
+public class TagsTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void loadTagsComponent_UsesProvidedClassLoader() {
+ final RuntimeException toThrow = new RuntimeException("UseClassLoader");
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("UseClassLoader");
+ Tags.loadTagsComponent(
+ new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) {
+ throw toThrow;
+ }
+ });
+ }
+
+ @Test
+ public void loadTagsComponent_IgnoresMissingClasses() {
+ ClassLoader classLoader =
+ new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) throws ClassNotFoundException {
+ throw new ClassNotFoundException();
+ }
+ };
+ assertThat(Tags.loadTagsComponent(classLoader).getClass().getName())
+ .isEqualTo("io.opencensus.tags.NoopTags$NoopTagsComponent");
+ }
+
+ // There is only one test that modifies tagging state in the Tags class, since the state is
+ // global, and it could affect other tests. NoopTagsTest has more thorough tests for tagging
+ // state.
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testState() {
+ // Test that setState ignores its input.
+ Tags.setState(TaggingState.ENABLED);
+ assertThat(Tags.getState()).isEqualTo(TaggingState.DISABLED);
+
+ // Test that setState cannot be called after getState.
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ Tags.setState(TaggingState.ENABLED);
+ }
+
+ @Test
+ public void defaultTagger() {
+ assertThat(Tags.getTagger()).isEqualTo(NoopTags.getNoopTagger());
+ }
+
+ @Test
+ public void defaultTagContextSerializer() {
+ assertThat(Tags.getTagPropagationComponent())
+ .isEqualTo(NoopTags.getNoopTagPropagationComponent());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/propagation/TagContextDeserializationExceptionTest.java b/api/src/test/java/io/opencensus/tags/propagation/TagContextDeserializationExceptionTest.java
new file mode 100644
index 00000000..750d5d45
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/propagation/TagContextDeserializationExceptionTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TagContextDeserializationException}. */
+@RunWith(JUnit4.class)
+public final class TagContextDeserializationExceptionTest {
+
+ @Test
+ public void createWithMessage() {
+ assertThat(new TagContextDeserializationException("my message").getMessage())
+ .isEqualTo("my message");
+ }
+
+ @Test
+ public void createWithMessageAndCause() {
+ IOException cause = new IOException();
+ TagContextDeserializationException exception =
+ new TagContextDeserializationException("my message", cause);
+ assertThat(exception.getMessage()).isEqualTo("my message");
+ assertThat(exception.getCause()).isEqualTo(cause);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/propagation/TagContextSerializationExceptionTest.java b/api/src/test/java/io/opencensus/tags/propagation/TagContextSerializationExceptionTest.java
new file mode 100644
index 00000000..54e9fab6
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/propagation/TagContextSerializationExceptionTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TagContextSerializationException}. */
+@RunWith(JUnit4.class)
+public final class TagContextSerializationExceptionTest {
+
+ @Test
+ public void createWithMessage() {
+ assertThat(new TagContextSerializationException("my message").getMessage())
+ .isEqualTo("my message");
+ }
+
+ @Test
+ public void createWithMessageAndCause() {
+ IOException cause = new IOException();
+ TagContextSerializationException exception =
+ new TagContextSerializationException("my message", cause);
+ assertThat(exception.getMessage()).isEqualTo("my message");
+ assertThat(exception.getCause()).isEqualTo(cause);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/tags/unsafe/ContextUtilsTest.java b/api/src/test/java/io/opencensus/tags/unsafe/ContextUtilsTest.java
new file mode 100644
index 00000000..c35c5dc4
--- /dev/null
+++ b/api/src/test/java/io/opencensus/tags/unsafe/ContextUtilsTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.tags.unsafe;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.grpc.Context;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ContextUtils}. */
+@RunWith(JUnit4.class)
+public final class ContextUtilsTest {
+ @Test
+ public void testContextKeyName() {
+ // Context.Key.toString() returns the name.
+ assertThat(ContextUtils.TAG_CONTEXT_KEY.toString()).isEqualTo("opencensus-tag-context-key");
+ }
+
+ @Test
+ public void testGetCurrentTagContext_DefaultContext() {
+ TagContext tags = ContextUtils.TAG_CONTEXT_KEY.get();
+ assertThat(tags).isNotNull();
+ assertThat(asList(tags)).isEmpty();
+ }
+
+ @Test
+ public void testGetCurrentTagContext_ContextSetToNull() {
+ Context orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, null).attach();
+ try {
+ TagContext tags = ContextUtils.TAG_CONTEXT_KEY.get();
+ assertThat(tags).isNotNull();
+ assertThat(asList(tags)).isEmpty();
+ } finally {
+ Context.current().detach(orig);
+ }
+ }
+
+ private static List<Tag> asList(TagContext tags) {
+ return Lists.newArrayList(InternalUtils.getTags(tags));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/AnnotationTest.java b/api/src/test/java/io/opencensus/trace/AnnotationTest.java
new file mode 100644
index 00000000..0db5d93c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/AnnotationTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Link}. */
+@RunWith(JUnit4.class)
+public class AnnotationTest {
+ @Test(expected = NullPointerException.class)
+ public void fromDescription_NullDescription() {
+ Annotation.fromDescription(null);
+ }
+
+ @Test
+ public void fromDescription() {
+ Annotation annotation = Annotation.fromDescription("MyAnnotationText");
+ assertThat(annotation.getDescription()).isEqualTo("MyAnnotationText");
+ assertThat(annotation.getAttributes().size()).isEqualTo(0);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void fromDescriptionAndAttributes_NullDescription() {
+ Annotation.fromDescriptionAndAttributes(null, Collections.<String, AttributeValue>emptyMap());
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void fromDescriptionAndAttributes_NullAttributes() {
+ Annotation.fromDescriptionAndAttributes("", null);
+ }
+
+ @Test
+ public void fromDescriptionAndAttributes() {
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ attributes.put(
+ "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ Annotation annotation = Annotation.fromDescriptionAndAttributes("MyAnnotationText", attributes);
+ assertThat(annotation.getDescription()).isEqualTo("MyAnnotationText");
+ assertThat(annotation.getAttributes()).isEqualTo(attributes);
+ }
+
+ @Test
+ public void fromDescriptionAndAttributes_EmptyAttributes() {
+ Annotation annotation =
+ Annotation.fromDescriptionAndAttributes(
+ "MyAnnotationText", Collections.<String, AttributeValue>emptyMap());
+ assertThat(annotation.getDescription()).isEqualTo("MyAnnotationText");
+ assertThat(annotation.getAttributes().size()).isEqualTo(0);
+ }
+
+ @Test
+ public void annotation_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ attributes.put(
+ "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ tester
+ .addEqualityGroup(
+ Annotation.fromDescription("MyAnnotationText"),
+ Annotation.fromDescriptionAndAttributes(
+ "MyAnnotationText", Collections.<String, AttributeValue>emptyMap()))
+ .addEqualityGroup(
+ Annotation.fromDescriptionAndAttributes("MyAnnotationText", attributes),
+ Annotation.fromDescriptionAndAttributes("MyAnnotationText", attributes))
+ .addEqualityGroup(Annotation.fromDescription("MyAnnotationText2"));
+ tester.testEquals();
+ }
+
+ @Test
+ public void annotation_ToString() {
+ Annotation annotation = Annotation.fromDescription("MyAnnotationText");
+ assertThat(annotation.toString()).contains("MyAnnotationText");
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ attributes.put(
+ "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ annotation = Annotation.fromDescriptionAndAttributes("MyAnnotationText2", attributes);
+ assertThat(annotation.toString()).contains("MyAnnotationText2");
+ assertThat(annotation.toString()).contains(attributes.toString());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/AttributeValueTest.java b/api/src/test/java/io/opencensus/trace/AttributeValueTest.java
new file mode 100644
index 00000000..05ef43c0
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/AttributeValueTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AttributeValue}. */
+@RunWith(JUnit4.class)
+public class AttributeValueTest {
+ @Test
+ public void stringAttributeValue() {
+ AttributeValue attribute = AttributeValue.stringAttributeValue("MyStringAttributeValue");
+ attribute.match(
+ new Function<String, Object>() {
+ @Override
+ @Nullable
+ public Object apply(String stringValue) {
+ assertThat(stringValue).isEqualTo("MyStringAttributeValue");
+ return null;
+ }
+ },
+ new Function<Boolean, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Boolean booleanValue) {
+ fail("Expected a String");
+ return null;
+ }
+ },
+ new Function<Long, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Long longValue) {
+ fail("Expected a String");
+ return null;
+ }
+ },
+ Functions.throwIllegalArgumentException());
+ }
+
+ @Test
+ public void booleanAttributeValue() {
+ AttributeValue attribute = AttributeValue.booleanAttributeValue(true);
+ attribute.match(
+ new Function<String, Object>() {
+ @Override
+ @Nullable
+ public Object apply(String stringValue) {
+ fail("Expected a Boolean");
+ return null;
+ }
+ },
+ new Function<Boolean, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Boolean booleanValue) {
+ assertThat(booleanValue).isTrue();
+ return null;
+ }
+ },
+ new Function<Long, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Long longValue) {
+ fail("Expected a Boolean");
+ return null;
+ }
+ },
+ Functions.throwIllegalArgumentException());
+ }
+
+ @Test
+ public void longAttributeValue() {
+ AttributeValue attribute = AttributeValue.longAttributeValue(123456L);
+ attribute.match(
+ new Function<String, Object>() {
+ @Override
+ @Nullable
+ public Object apply(String stringValue) {
+ fail("Expected a Long");
+ return null;
+ }
+ },
+ new Function<Boolean, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Boolean booleanValue) {
+ fail("Expected a Long");
+ return null;
+ }
+ },
+ new Function<Long, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Long longValue) {
+ assertThat(longValue).isEqualTo(123456L);
+ return null;
+ }
+ },
+ Functions.throwIllegalArgumentException());
+ }
+
+ @Test
+ public void doubleAttributeValue() {
+ AttributeValue attribute = AttributeValue.doubleAttributeValue(1.23456);
+ attribute.match(
+ new Function<String, Object>() {
+ @Override
+ @Nullable
+ public Object apply(String stringValue) {
+ fail("Expected a Double");
+ return null;
+ }
+ },
+ new Function<Boolean, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Boolean booleanValue) {
+ fail("Expected a Double");
+ return null;
+ }
+ },
+ new Function<Long, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Long longValue) {
+ fail("Expected a Double");
+ return null;
+ }
+ },
+ new Function<Double, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Double doubleValue) {
+ assertThat(doubleValue).isEqualTo(1.23456);
+ return null;
+ }
+ },
+ Functions.throwIllegalArgumentException());
+ }
+
+ @Test
+ public void doubleAttributeValue_DeprecatedMatchFunction() {
+ AttributeValue attribute = AttributeValue.doubleAttributeValue(1.23456);
+ attribute.match(
+ new Function<String, Object>() {
+ @Override
+ @Nullable
+ public Object apply(String stringValue) {
+ fail("Expected a Double");
+ return null;
+ }
+ },
+ new Function<Boolean, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Boolean booleanValue) {
+ fail("Expected a Double");
+ return null;
+ }
+ },
+ new Function<Long, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Long longValue) {
+ fail("Expected a Double");
+ return null;
+ }
+ },
+ new Function<Object, Object>() {
+ @Override
+ @Nullable
+ public Object apply(Object value) {
+ assertThat(value).isEqualTo(1.23456);
+ return null;
+ }
+ });
+ }
+
+ @Test
+ public void attributeValue_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(
+ AttributeValue.stringAttributeValue("MyStringAttributeValue"),
+ AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ tester.addEqualityGroup(AttributeValue.stringAttributeValue("MyStringAttributeDiffValue"));
+ tester.addEqualityGroup(
+ AttributeValue.booleanAttributeValue(true), AttributeValue.booleanAttributeValue(true));
+ tester.addEqualityGroup(AttributeValue.booleanAttributeValue(false));
+ tester.addEqualityGroup(
+ AttributeValue.longAttributeValue(123456L), AttributeValue.longAttributeValue(123456L));
+ tester.addEqualityGroup(AttributeValue.longAttributeValue(1234567L));
+ tester.addEqualityGroup(
+ AttributeValue.doubleAttributeValue(1.23456), AttributeValue.doubleAttributeValue(1.23456));
+ tester.addEqualityGroup(AttributeValue.doubleAttributeValue(1.234567));
+ tester.testEquals();
+ }
+
+ @Test
+ public void attributeValue_ToString() {
+ AttributeValue attribute = AttributeValue.stringAttributeValue("MyStringAttributeValue");
+ assertThat(attribute.toString()).contains("MyStringAttributeValue");
+ attribute = AttributeValue.booleanAttributeValue(true);
+ assertThat(attribute.toString()).contains("true");
+ attribute = AttributeValue.longAttributeValue(123456L);
+ assertThat(attribute.toString()).contains("123456");
+ attribute = AttributeValue.doubleAttributeValue(1.23456);
+ assertThat(attribute.toString()).contains("1.23456");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/BlankSpanTest.java b/api/src/test/java/io/opencensus/trace/BlankSpanTest.java
new file mode 100644
index 00000000..185a5acd
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/BlankSpanTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BlankSpan}. */
+@RunWith(JUnit4.class)
+public class BlankSpanTest {
+ @Test
+ public void hasInvalidContextAndDefaultSpanOptions() {
+ assertThat(BlankSpan.INSTANCE.getContext()).isEqualTo(SpanContext.INVALID);
+ assertThat(BlankSpan.INSTANCE.getOptions().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void doNotCrash() {
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ attributes.put(
+ "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ Map<String, AttributeValue> multipleAttributes = new HashMap<String, AttributeValue>();
+ multipleAttributes.put(
+ "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ multipleAttributes.put("MyBooleanAttributeKey", AttributeValue.booleanAttributeValue(true));
+ multipleAttributes.put("MyLongAttributeKey", AttributeValue.longAttributeValue(123));
+ // Tests only that all the methods are not crashing/throwing errors.
+ BlankSpan.INSTANCE.putAttribute(
+ "MyStringAttributeKey2", AttributeValue.stringAttributeValue("MyStringAttributeValue2"));
+ BlankSpan.INSTANCE.addAttributes(attributes);
+ BlankSpan.INSTANCE.addAttributes(multipleAttributes);
+ BlankSpan.INSTANCE.addAnnotation("MyAnnotation");
+ BlankSpan.INSTANCE.addAnnotation("MyAnnotation", attributes);
+ BlankSpan.INSTANCE.addAnnotation("MyAnnotation", multipleAttributes);
+ BlankSpan.INSTANCE.addAnnotation(Annotation.fromDescription("MyAnnotation"));
+ BlankSpan.INSTANCE.addNetworkEvent(NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).build());
+ BlankSpan.INSTANCE.addMessageEvent(MessageEvent.builder(MessageEvent.Type.SENT, 1L).build());
+ BlankSpan.INSTANCE.addLink(
+ Link.fromSpanContext(SpanContext.INVALID, Link.Type.CHILD_LINKED_SPAN));
+ BlankSpan.INSTANCE.setStatus(Status.OK);
+ BlankSpan.INSTANCE.end(EndSpanOptions.DEFAULT);
+ BlankSpan.INSTANCE.end();
+ }
+
+ @Test
+ public void blankSpan_ToString() {
+ assertThat(BlankSpan.INSTANCE.toString()).isEqualTo("BlankSpan");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/CurrentSpanUtilsTest.java b/api/src/test/java/io/opencensus/trace/CurrentSpanUtilsTest.java
new file mode 100644
index 00000000..6b16c3d0
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/CurrentSpanUtilsTest.java
@@ -0,0 +1,294 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.unsafe.ContextUtils;
+import java.util.concurrent.Callable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link CurrentSpanUtils}. */
+@RunWith(JUnit4.class)
+public class CurrentSpanUtilsTest {
+ @Mock private Span span;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ // TODO(bdrutu): When update to junit 4.13 use assertThrows instead of this.
+ private void executeRunnableAndExpectError(Runnable runnable, Throwable error) {
+ boolean called = false;
+ try {
+ CurrentSpanUtils.withSpan(span, true, runnable).run();
+ } catch (Throwable e) {
+ assertThat(e).isEqualTo(error);
+ called = true;
+ }
+ assertThat(called).isTrue();
+ }
+
+ // TODO(bdrutu): When update to junit 4.13 use assertThrows instead of this.
+ private void executeCallableAndExpectError(Callable<Object> callable, Throwable error) {
+ boolean called = false;
+ try {
+ CurrentSpanUtils.withSpan(span, true, callable).call();
+ } catch (Throwable e) {
+ assertThat(e).isEqualTo(error);
+ called = true;
+ }
+ assertThat(called).isTrue();
+ }
+
+ @Test
+ public void getCurrentSpan_WhenNoContext() {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void getCurrentSpan() {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Context origContext = Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, span).attach();
+ // Make sure context is detached even if test fails.
+ try {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ } finally {
+ Context.current().detach(origContext);
+ }
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpan_CloseDetaches() {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Scope ws = CurrentSpanUtils.withSpan(span, false);
+ try {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ } finally {
+ ws.close();
+ }
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ verifyZeroInteractions(span);
+ }
+
+ @Test
+ public void withSpan_CloseDetachesAndEndsSpan() {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Scope ss = CurrentSpanUtils.withSpan(span, true);
+ try {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ } finally {
+ ss.close();
+ }
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ verify(span).end(same(EndSpanOptions.DEFAULT));
+ }
+
+ @Test
+ public void withSpanRunnable() {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ }
+ };
+ CurrentSpanUtils.withSpan(span, false, runnable).run();
+ verifyZeroInteractions(span);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanRunnable_EndSpan() {
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ }
+ };
+ CurrentSpanUtils.withSpan(span, true, runnable).run();
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanRunnable_WithError() {
+ final AssertionError error = new AssertionError("MyError");
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ throw error;
+ }
+ };
+ executeRunnableAndExpectError(runnable, error);
+ verify(span).setStatus(Status.UNKNOWN.withDescription("MyError"));
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanRunnable_WithErrorNoMessage() {
+ final AssertionError error = new AssertionError();
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ throw error;
+ }
+ };
+ executeRunnableAndExpectError(runnable, error);
+ verify(span).setStatus(Status.UNKNOWN.withDescription("AssertionError"));
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanCallable() throws Exception {
+ final Object ret = new Object();
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Callable<Object> callable =
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ return ret;
+ }
+ };
+ assertThat(CurrentSpanUtils.withSpan(span, false, callable).call()).isEqualTo(ret);
+ verifyZeroInteractions(span);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanCallable_EndSpan() throws Exception {
+ final Object ret = new Object();
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Callable<Object> callable =
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ return ret;
+ }
+ };
+ assertThat(CurrentSpanUtils.withSpan(span, true, callable).call()).isEqualTo(ret);
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanCallable_WithException() {
+ final Exception exception = new Exception("MyException");
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Callable<Object> callable =
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ throw exception;
+ }
+ };
+ executeCallableAndExpectError(callable, exception);
+ verify(span).setStatus(Status.UNKNOWN.withDescription("MyException"));
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanCallable_WithExceptionNoMessage() {
+ final Exception exception = new Exception();
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Callable<Object> callable =
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ throw exception;
+ }
+ };
+ executeCallableAndExpectError(callable, exception);
+ verify(span).setStatus(Status.UNKNOWN.withDescription("Exception"));
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanCallable_WithError() {
+ final AssertionError error = new AssertionError("MyError");
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Callable<Object> callable =
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ throw error;
+ }
+ };
+ executeCallableAndExpectError(callable, error);
+ verify(span).setStatus(Status.UNKNOWN.withDescription("MyError"));
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+
+ @Test
+ public void withSpanCallable_WithErrorNoMessage() {
+ final AssertionError error = new AssertionError();
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ Callable<Object> callable =
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ // When we run the runnable we will have the span in the current Context.
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isSameAs(span);
+ throw error;
+ }
+ };
+ executeCallableAndExpectError(callable, error);
+ verify(span).setStatus(Status.UNKNOWN.withDescription("AssertionError"));
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(CurrentSpanUtils.getCurrentSpan()).isNull();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/EndSpanOptionsTest.java b/api/src/test/java/io/opencensus/trace/EndSpanOptionsTest.java
new file mode 100644
index 00000000..b6ab8e0e
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/EndSpanOptionsTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link EndSpanOptions}. */
+@RunWith(JUnit4.class)
+public class EndSpanOptionsTest {
+ @Test
+ public void endSpanOptions_DefaultOptions() {
+ assertThat(EndSpanOptions.DEFAULT.getStatus()).isNull();
+ assertThat(EndSpanOptions.DEFAULT.getSampleToLocalSpanStore()).isFalse();
+ }
+
+ @Test
+ public void setStatus_Ok() {
+ EndSpanOptions endSpanOptions = EndSpanOptions.builder().setStatus(Status.OK).build();
+ assertThat(endSpanOptions.getStatus()).isEqualTo(Status.OK);
+ }
+
+ @Test
+ public void setStatus_Error() {
+ EndSpanOptions endSpanOptions =
+ EndSpanOptions.builder()
+ .setStatus(Status.CANCELLED.withDescription("ThisIsAnError"))
+ .build();
+ assertThat(endSpanOptions.getStatus())
+ .isEqualTo(Status.CANCELLED.withDescription("ThisIsAnError"));
+ }
+
+ @Test
+ public void setSampleToLocalSpanStore() {
+ EndSpanOptions endSpanOptions =
+ EndSpanOptions.builder().setSampleToLocalSpanStore(true).build();
+ assertThat(endSpanOptions.getSampleToLocalSpanStore()).isTrue();
+ }
+
+ @Test
+ public void endSpanOptions_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(
+ EndSpanOptions.builder()
+ .setStatus(Status.CANCELLED.withDescription("ThisIsAnError"))
+ .build(),
+ EndSpanOptions.builder()
+ .setStatus(Status.CANCELLED.withDescription("ThisIsAnError"))
+ .build());
+ tester.addEqualityGroup(EndSpanOptions.builder().build(), EndSpanOptions.DEFAULT);
+ tester.testEquals();
+ }
+
+ @Test
+ public void endSpanOptions_ToString() {
+ EndSpanOptions endSpanOptions =
+ EndSpanOptions.builder()
+ .setStatus(Status.CANCELLED.withDescription("ThisIsAnError"))
+ .build();
+ assertThat(endSpanOptions.toString()).contains("ThisIsAnError");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/LinkTest.java b/api/src/test/java/io/opencensus/trace/LinkTest.java
new file mode 100644
index 00000000..5c1ebf5d
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/LinkTest.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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.trace.Link.Type;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Link}. */
+@RunWith(JUnit4.class)
+public class LinkTest {
+ private final Map<String, AttributeValue> attributesMap = new HashMap<String, AttributeValue>();
+ private final Random random = new Random(1234);
+ private final SpanContext spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+
+ @Before
+ public void setUp() {
+ attributesMap.put("MyAttributeKey0", AttributeValue.stringAttributeValue("MyStringAttribute"));
+ attributesMap.put("MyAttributeKey1", AttributeValue.longAttributeValue(10));
+ attributesMap.put("MyAttributeKey2", AttributeValue.booleanAttributeValue(true));
+ }
+
+ @Test
+ public void fromSpanContext_ChildLink() {
+ Link link = Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN);
+ assertThat(link.getTraceId()).isEqualTo(spanContext.getTraceId());
+ assertThat(link.getSpanId()).isEqualTo(spanContext.getSpanId());
+ assertThat(link.getType()).isEqualTo(Type.CHILD_LINKED_SPAN);
+ }
+
+ @Test
+ public void fromSpanContext_ChildLink_WithAttributes() {
+ Link link = Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN, attributesMap);
+ assertThat(link.getTraceId()).isEqualTo(spanContext.getTraceId());
+ assertThat(link.getSpanId()).isEqualTo(spanContext.getSpanId());
+ assertThat(link.getType()).isEqualTo(Type.CHILD_LINKED_SPAN);
+ assertThat(link.getAttributes()).isEqualTo(attributesMap);
+ }
+
+ @Test
+ public void fromSpanContext_ParentLink() {
+ Link link = Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN);
+ assertThat(link.getTraceId()).isEqualTo(spanContext.getTraceId());
+ assertThat(link.getSpanId()).isEqualTo(spanContext.getSpanId());
+ assertThat(link.getType()).isEqualTo(Type.PARENT_LINKED_SPAN);
+ }
+
+ @Test
+ public void fromSpanContext_ParentLink_WithAttributes() {
+ Link link = Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN, attributesMap);
+ assertThat(link.getTraceId()).isEqualTo(spanContext.getTraceId());
+ assertThat(link.getSpanId()).isEqualTo(spanContext.getSpanId());
+ assertThat(link.getType()).isEqualTo(Type.PARENT_LINKED_SPAN);
+ assertThat(link.getAttributes()).isEqualTo(attributesMap);
+ }
+
+ @Test
+ public void link_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester
+ .addEqualityGroup(
+ Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN),
+ Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN))
+ .addEqualityGroup(
+ Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN),
+ Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN))
+ .addEqualityGroup(Link.fromSpanContext(SpanContext.INVALID, Type.CHILD_LINKED_SPAN))
+ .addEqualityGroup(Link.fromSpanContext(SpanContext.INVALID, Type.PARENT_LINKED_SPAN))
+ .addEqualityGroup(
+ Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN, attributesMap),
+ Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN, attributesMap));
+ tester.testEquals();
+ }
+
+ @Test
+ public void link_ToString() {
+ Link link = Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN, attributesMap);
+ assertThat(link.toString()).contains(spanContext.getTraceId().toString());
+ assertThat(link.toString()).contains(spanContext.getSpanId().toString());
+ assertThat(link.toString()).contains("CHILD_LINKED_SPAN");
+ assertThat(link.toString()).contains(attributesMap.toString());
+ link = Link.fromSpanContext(spanContext, Type.PARENT_LINKED_SPAN, attributesMap);
+ assertThat(link.toString()).contains(spanContext.getTraceId().toString());
+ assertThat(link.toString()).contains(spanContext.getSpanId().toString());
+ assertThat(link.toString()).contains("PARENT_LINKED_SPAN");
+ assertThat(link.toString()).contains(attributesMap.toString());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/LowerCaseBase16EncodingTest.java b/api/src/test/java/io/opencensus/trace/LowerCaseBase16EncodingTest.java
new file mode 100644
index 00000000..3444d2b3
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/LowerCaseBase16EncodingTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.nio.charset.Charset;
+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.trace.LowerCaseBase16Encoding}. */
+@RunWith(JUnit4.class)
+public class LowerCaseBase16EncodingTest {
+ private static final Charset CHARSET = Charset.forName("UTF-8");
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void valid_EncodeDecode() {
+ testEncoding("", "");
+ testEncoding("f", "66");
+ testEncoding("fo", "666f");
+ testEncoding("foo", "666f6f");
+ testEncoding("foob", "666f6f62");
+ testEncoding("fooba", "666f6f6261");
+ testEncoding("foobar", "666f6f626172");
+ }
+
+ @Test
+ public void invalidDecodings_UnrecongnizedCharacters() {
+ // These contain bytes not in the decoding.
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Invalid character g");
+ LowerCaseBase16Encoding.decodeToBytes("efhg");
+ }
+
+ @Test
+ public void invalidDecodings_InvalidInputLength() {
+ // Valid base16 strings always have an even length.
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Invalid input length 3");
+ LowerCaseBase16Encoding.decodeToBytes("abc");
+ }
+
+ @Test
+ public void invalidDecodings_InvalidInputLengthAndCharacter() {
+ // These have a combination of invalid length and unrecognized characters.
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Invalid input length 1");
+ LowerCaseBase16Encoding.decodeToBytes("?");
+ }
+
+ private static void testEncoding(String decoded, String encoded) {
+ testEncodes(decoded, encoded);
+ testDecodes(encoded, decoded);
+ }
+
+ private static void testEncodes(String decoded, String encoded) {
+ assertThat(LowerCaseBase16Encoding.encodeToString(decoded.getBytes(CHARSET)))
+ .isEqualTo(encoded);
+ }
+
+ private static void testDecodes(String encoded, String decoded) {
+ assertThat(LowerCaseBase16Encoding.decodeToBytes(encoded)).isEqualTo(decoded.getBytes(CHARSET));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/MessageEventTest.java b/api/src/test/java/io/opencensus/trace/MessageEventTest.java
new file mode 100644
index 00000000..fde32fe6
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/MessageEventTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MessageEvent}. */
+@RunWith(JUnit4.class)
+public class MessageEventTest {
+ @Test(expected = NullPointerException.class)
+ public void buildMessageEvent_NullType() {
+ MessageEvent.builder(null, 1L).build();
+ }
+
+ @Test
+ public void buildMessageEvent_WithRequiredFields() {
+ MessageEvent messageEvent = MessageEvent.builder(MessageEvent.Type.SENT, 1L).build();
+ assertThat(messageEvent.getType()).isEqualTo(MessageEvent.Type.SENT);
+ assertThat(messageEvent.getMessageId()).isEqualTo(1L);
+ assertThat(messageEvent.getUncompressedMessageSize()).isEqualTo(0L);
+ }
+
+ @Test
+ public void buildMessageEvent_WithUncompressedMessageSize() {
+ MessageEvent messageEvent =
+ MessageEvent.builder(MessageEvent.Type.SENT, 1L).setUncompressedMessageSize(123L).build();
+ assertThat(messageEvent.getType()).isEqualTo(MessageEvent.Type.SENT);
+ assertThat(messageEvent.getMessageId()).isEqualTo(1L);
+ assertThat(messageEvent.getUncompressedMessageSize()).isEqualTo(123L);
+ }
+
+ @Test
+ public void buildMessageEvent_WithCompressedMessageSize() {
+ MessageEvent messageEvent =
+ MessageEvent.builder(MessageEvent.Type.SENT, 1L).setCompressedMessageSize(123L).build();
+ assertThat(messageEvent.getType()).isEqualTo(MessageEvent.Type.SENT);
+ assertThat(messageEvent.getMessageId()).isEqualTo(1L);
+ assertThat(messageEvent.getCompressedMessageSize()).isEqualTo(123L);
+ }
+
+ @Test
+ public void buildMessageEvent_WithAllValues() {
+ MessageEvent messageEvent =
+ MessageEvent.builder(MessageEvent.Type.RECEIVED, 1L)
+ .setUncompressedMessageSize(123L)
+ .setCompressedMessageSize(63L)
+ .build();
+ assertThat(messageEvent.getType()).isEqualTo(MessageEvent.Type.RECEIVED);
+ assertThat(messageEvent.getMessageId()).isEqualTo(1L);
+ assertThat(messageEvent.getUncompressedMessageSize()).isEqualTo(123L);
+ assertThat(messageEvent.getCompressedMessageSize()).isEqualTo(63L);
+ }
+
+ @Test
+ public void messageEvent_ToString() {
+ MessageEvent messageEvent =
+ MessageEvent.builder(MessageEvent.Type.SENT, 1L)
+ .setUncompressedMessageSize(123L)
+ .setCompressedMessageSize(63L)
+ .build();
+ assertThat(messageEvent.toString()).contains("type=SENT");
+ assertThat(messageEvent.toString()).contains("messageId=1");
+ assertThat(messageEvent.toString()).contains("compressedMessageSize=63");
+ assertThat(messageEvent.toString()).contains("uncompressedMessageSize=123");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/NetworkEventTest.java b/api/src/test/java/io/opencensus/trace/NetworkEventTest.java
new file mode 100644
index 00000000..8c132377
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/NetworkEventTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NetworkEvent}. */
+@RunWith(JUnit4.class)
+public class NetworkEventTest {
+ @Test(expected = NullPointerException.class)
+ public void buildNetworkEvent_NullType() {
+ NetworkEvent.builder(null, 1L).build();
+ }
+
+ @Test
+ public void buildNetworkEvent_WithRequiredFields() {
+ NetworkEvent networkEvent = NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).build();
+ assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+ assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+ assertThat(networkEvent.getKernelTimestamp()).isNull();
+ assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(0L);
+ }
+
+ @Test
+ public void buildNetworkEvent_WithTimestamp() {
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.SENT, 1L)
+ .setKernelTimestamp(Timestamp.fromMillis(123456L))
+ .build();
+ assertThat(networkEvent.getKernelTimestamp()).isEqualTo(Timestamp.fromMillis(123456L));
+ assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+ assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+ assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(0L);
+ }
+
+ @Test
+ public void buildNetworkEvent_WithUncompressedMessageSize() {
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).setUncompressedMessageSize(123L).build();
+ assertThat(networkEvent.getKernelTimestamp()).isNull();
+ assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+ assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+ assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(123L);
+ assertThat(networkEvent.getMessageSize()).isEqualTo(123L);
+ }
+
+ @Test
+ public void buildNetworkEvent_WithMessageSize() {
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).setMessageSize(123L).build();
+ assertThat(networkEvent.getKernelTimestamp()).isNull();
+ assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+ assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+ assertThat(networkEvent.getMessageSize()).isEqualTo(123L);
+ assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(123L);
+ }
+
+ @Test
+ public void buildNetworkEvent_WithCompressedMessageSize() {
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).setCompressedMessageSize(123L).build();
+ assertThat(networkEvent.getKernelTimestamp()).isNull();
+ assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.SENT);
+ assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+ assertThat(networkEvent.getCompressedMessageSize()).isEqualTo(123L);
+ }
+
+ @Test
+ public void buildNetworkEvent_WithAllValues() {
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.RECV, 1L)
+ .setKernelTimestamp(Timestamp.fromMillis(123456L))
+ .setUncompressedMessageSize(123L)
+ .setCompressedMessageSize(63L)
+ .build();
+ assertThat(networkEvent.getKernelTimestamp()).isEqualTo(Timestamp.fromMillis(123456L));
+ assertThat(networkEvent.getType()).isEqualTo(NetworkEvent.Type.RECV);
+ assertThat(networkEvent.getMessageId()).isEqualTo(1L);
+ assertThat(networkEvent.getUncompressedMessageSize()).isEqualTo(123L);
+ // Test that getMessageSize returns same as getUncompressedMessageSize();
+ assertThat(networkEvent.getMessageSize()).isEqualTo(123L);
+ assertThat(networkEvent.getCompressedMessageSize()).isEqualTo(63L);
+ }
+
+ @Test
+ public void networkEvent_ToString() {
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.SENT, 1L)
+ .setKernelTimestamp(Timestamp.fromMillis(123456L))
+ .setUncompressedMessageSize(123L)
+ .setCompressedMessageSize(63L)
+ .build();
+ assertThat(networkEvent.toString()).contains(Timestamp.fromMillis(123456L).toString());
+ assertThat(networkEvent.toString()).contains("type=SENT");
+ assertThat(networkEvent.toString()).contains("messageId=1");
+ assertThat(networkEvent.toString()).contains("compressedMessageSize=63");
+ assertThat(networkEvent.toString()).contains("uncompressedMessageSize=123");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/NoopSpan.java b/api/src/test/java/io/opencensus/trace/NoopSpan.java
new file mode 100644
index 00000000..a21a8aac
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/NoopSpan.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.trace;
+
+import io.opencensus.internal.Utils;
+import java.util.EnumSet;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Class to be used in tests where an implementation for the Span is needed.
+ *
+ * <p>Not final to allow Mockito to "spy" this class.
+ */
+public class NoopSpan extends Span {
+
+ /** Creates a new {@code NoopSpan}. */
+ public NoopSpan(SpanContext context, @Nullable EnumSet<Options> options) {
+ super(Utils.checkNotNull(context, "context"), options);
+ }
+
+ @Override
+ public void putAttributes(Map<String, AttributeValue> attributes) {
+ Utils.checkNotNull(attributes, "attributes");
+ }
+
+ @Override
+ public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+ Utils.checkNotNull(description, "description");
+ Utils.checkNotNull(attributes, "attributes");
+ }
+
+ @Override
+ public void addAnnotation(Annotation annotation) {
+ Utils.checkNotNull(annotation, "annotation");
+ }
+
+ @Override
+ public void addNetworkEvent(NetworkEvent networkEvent) {}
+
+ @Override
+ public void addMessageEvent(MessageEvent messageEvent) {
+ Utils.checkNotNull(messageEvent, "messageEvent");
+ }
+
+ @Override
+ public void addLink(Link link) {
+ Utils.checkNotNull(link, "link");
+ }
+
+ @Override
+ public void end(EndSpanOptions options) {
+ Utils.checkNotNull(options, "options");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/SpanBuilderTest.java b/api/src/test/java/io/opencensus/trace/SpanBuilderTest.java
new file mode 100644
index 00000000..839c8945
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/SpanBuilderTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.common.Scope;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+import java.util.concurrent.Callable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link SpanBuilder}. */
+@RunWith(JUnit4.class)
+// Need to suppress warnings for MustBeClosed because Java-6 does not support try-with-resources.
+@SuppressWarnings("MustBeClosedChecker")
+public class SpanBuilderTest {
+ private final Tracer tracer = Tracing.getTracer();
+ @Mock private SpanBuilder spanBuilder;
+ @Mock private Span span;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(spanBuilder.startSpan()).thenReturn(span);
+ }
+
+ @Test
+ public void startScopedSpan() {
+ assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ Scope scope = spanBuilder.startScopedSpan();
+ try {
+ assertThat(tracer.getCurrentSpan()).isSameAs(span);
+ } finally {
+ scope.close();
+ }
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test
+ public void startSpanAndRun() {
+ assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ spanBuilder.startSpanAndRun(
+ new Runnable() {
+ @Override
+ public void run() {
+ assertThat(tracer.getCurrentSpan()).isSameAs(span);
+ }
+ });
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test
+ public void startSpanAndCall() throws Exception {
+ final Object ret = new Object();
+ assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ assertThat(
+ spanBuilder.startSpanAndCall(
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ assertThat(tracer.getCurrentSpan()).isSameAs(span);
+ return ret;
+ }
+ }))
+ .isEqualTo(ret);
+ verify(span).end(EndSpanOptions.DEFAULT);
+ assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test
+ public void doNotCrash_NoopImplementation() throws Exception {
+ SpanBuilder spanBuilder = tracer.spanBuilder("MySpanName");
+ spanBuilder.setParentLinks(Collections.<Span>emptyList());
+ spanBuilder.setRecordEvents(true);
+ spanBuilder.setSampler(Samplers.alwaysSample());
+ spanBuilder.setSpanKind(Kind.SERVER);
+ assertThat(spanBuilder.startSpan()).isSameAs(BlankSpan.INSTANCE);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/SpanContextTest.java b/api/src/test/java/io/opencensus/trace/SpanContextTest.java
new file mode 100644
index 00000000..54e188c8
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/SpanContextTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SpanContext}. */
+@RunWith(JUnit4.class)
+public class SpanContextTest {
+ private static final byte[] firstTraceIdBytes =
+ new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'a'};
+ private static final byte[] secondTraceIdBytes =
+ new byte[] {0, 0, 0, 0, 0, 0, 0, '0', 0, 0, 0, 0, 0, 0, 0, 0};
+ private static final byte[] firstSpanIdBytes = new byte[] {0, 0, 0, 0, 0, 0, 0, 'a'};
+ private static final byte[] secondSpanIdBytes = new byte[] {'0', 0, 0, 0, 0, 0, 0, 0};
+ private static final Tracestate firstTracestate = Tracestate.builder().set("foo", "bar").build();
+ private static final Tracestate secondTracestate = Tracestate.builder().set("foo", "baz").build();
+ private static final SpanContext first =
+ SpanContext.create(
+ TraceId.fromBytes(firstTraceIdBytes),
+ SpanId.fromBytes(firstSpanIdBytes),
+ TraceOptions.DEFAULT,
+ firstTracestate);
+ private static final SpanContext second =
+ SpanContext.create(
+ TraceId.fromBytes(secondTraceIdBytes),
+ SpanId.fromBytes(secondSpanIdBytes),
+ TraceOptions.builder().setIsSampled(true).build(),
+ secondTracestate);
+
+ @Test
+ public void invalidSpanContext() {
+ assertThat(SpanContext.INVALID.getTraceId()).isEqualTo(TraceId.INVALID);
+ assertThat(SpanContext.INVALID.getSpanId()).isEqualTo(SpanId.INVALID);
+ assertThat(SpanContext.INVALID.getTraceOptions()).isEqualTo(TraceOptions.DEFAULT);
+ }
+
+ @Test
+ public void isValid() {
+ assertThat(SpanContext.INVALID.isValid()).isFalse();
+ assertThat(
+ SpanContext.create(
+ TraceId.fromBytes(firstTraceIdBytes), SpanId.INVALID, TraceOptions.DEFAULT)
+ .isValid())
+ .isFalse();
+ assertThat(
+ SpanContext.create(
+ TraceId.INVALID, SpanId.fromBytes(firstSpanIdBytes), TraceOptions.DEFAULT)
+ .isValid())
+ .isFalse();
+ assertThat(first.isValid()).isTrue();
+ assertThat(second.isValid()).isTrue();
+ }
+
+ @Test
+ public void getTraceId() {
+ assertThat(first.getTraceId()).isEqualTo(TraceId.fromBytes(firstTraceIdBytes));
+ assertThat(second.getTraceId()).isEqualTo(TraceId.fromBytes(secondTraceIdBytes));
+ }
+
+ @Test
+ public void getSpanId() {
+ assertThat(first.getSpanId()).isEqualTo(SpanId.fromBytes(firstSpanIdBytes));
+ assertThat(second.getSpanId()).isEqualTo(SpanId.fromBytes(secondSpanIdBytes));
+ }
+
+ @Test
+ public void getTraceOptions() {
+ assertThat(first.getTraceOptions()).isEqualTo(TraceOptions.DEFAULT);
+ assertThat(second.getTraceOptions())
+ .isEqualTo(TraceOptions.builder().setIsSampled(true).build());
+ }
+
+ @Test
+ public void getTracestate() {
+ assertThat(first.getTracestate()).isEqualTo(firstTracestate);
+ assertThat(second.getTracestate()).isEqualTo(secondTracestate);
+ }
+
+ @Test
+ public void spanContext_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(
+ first,
+ SpanContext.create(
+ TraceId.fromBytes(firstTraceIdBytes),
+ SpanId.fromBytes(firstSpanIdBytes),
+ TraceOptions.DEFAULT),
+ SpanContext.create(
+ TraceId.fromBytes(firstTraceIdBytes),
+ SpanId.fromBytes(firstSpanIdBytes),
+ TraceOptions.builder().setIsSampled(false).build(),
+ firstTracestate));
+ tester.addEqualityGroup(
+ second,
+ SpanContext.create(
+ TraceId.fromBytes(secondTraceIdBytes),
+ SpanId.fromBytes(secondSpanIdBytes),
+ TraceOptions.builder().setIsSampled(true).build(),
+ secondTracestate));
+ tester.testEquals();
+ }
+
+ @Test
+ public void spanContext_ToString() {
+ assertThat(first.toString()).contains(TraceId.fromBytes(firstTraceIdBytes).toString());
+ assertThat(first.toString()).contains(SpanId.fromBytes(firstSpanIdBytes).toString());
+ assertThat(first.toString()).contains(TraceOptions.DEFAULT.toString());
+ assertThat(second.toString()).contains(TraceId.fromBytes(secondTraceIdBytes).toString());
+ assertThat(second.toString()).contains(SpanId.fromBytes(secondSpanIdBytes).toString());
+ assertThat(second.toString())
+ .contains(TraceOptions.builder().setIsSampled(true).build().toString());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/SpanIdTest.java b/api/src/test/java/io/opencensus/trace/SpanIdTest.java
new file mode 100644
index 00000000..4a5bc2ae
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/SpanIdTest.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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SpanId}. */
+@RunWith(JUnit4.class)
+public class SpanIdTest {
+ private static final byte[] firstBytes = new byte[] {0, 0, 0, 0, 0, 0, 0, 'a'};
+ private static final byte[] secondBytes = new byte[] {(byte) 0xFF, 0, 0, 0, 0, 0, 0, 'A'};
+ private static final SpanId first = SpanId.fromBytes(firstBytes);
+ private static final SpanId second = SpanId.fromBytes(secondBytes);
+
+ @Test
+ public void invalidSpanId() {
+ assertThat(SpanId.INVALID.getBytes()).isEqualTo(new byte[8]);
+ }
+
+ @Test
+ public void isValid() {
+ assertThat(SpanId.INVALID.isValid()).isFalse();
+ assertThat(first.isValid()).isTrue();
+ assertThat(second.isValid()).isTrue();
+ }
+
+ @Test
+ public void fromLowerBase16() {
+ assertThat(SpanId.fromLowerBase16("0000000000000000")).isEqualTo(SpanId.INVALID);
+ assertThat(SpanId.fromLowerBase16("0000000000000061")).isEqualTo(first);
+ assertThat(SpanId.fromLowerBase16("ff00000000000041")).isEqualTo(second);
+ }
+
+ @Test
+ public void toLowerBase16() {
+ assertThat(SpanId.INVALID.toLowerBase16()).isEqualTo("0000000000000000");
+ assertThat(first.toLowerBase16()).isEqualTo("0000000000000061");
+ assertThat(second.toLowerBase16()).isEqualTo("ff00000000000041");
+ }
+
+ @Test
+ public void getBytes() {
+ assertThat(first.getBytes()).isEqualTo(firstBytes);
+ assertThat(second.getBytes()).isEqualTo(secondBytes);
+ }
+
+ @Test
+ public void traceId_CompareTo() {
+ assertThat(first.compareTo(second)).isGreaterThan(0);
+ assertThat(second.compareTo(first)).isLessThan(0);
+ assertThat(first.compareTo(SpanId.fromBytes(firstBytes))).isEqualTo(0);
+ }
+
+ @Test
+ public void traceId_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(SpanId.INVALID, SpanId.INVALID);
+ tester.addEqualityGroup(first, SpanId.fromBytes(Arrays.copyOf(firstBytes, firstBytes.length)));
+ tester.addEqualityGroup(
+ second, SpanId.fromBytes(Arrays.copyOf(secondBytes, secondBytes.length)));
+ tester.testEquals();
+ }
+
+ @Test
+ public void traceId_ToString() {
+ assertThat(SpanId.INVALID.toString()).contains("0000000000000000");
+ assertThat(first.toString()).contains("0000000000000061");
+ assertThat(second.toString()).contains("ff00000000000041");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/SpanTest.java b/api/src/test/java/io/opencensus/trace/SpanTest.java
new file mode 100644
index 00000000..f7546ca4
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/SpanTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link Span}. */
+@RunWith(JUnit4.class)
+public class SpanTest {
+ private Random random;
+ private SpanContext spanContext;
+ private SpanContext notSampledSpanContext;
+ private EnumSet<Span.Options> spanOptions;
+
+ @Before
+ public void setUp() {
+ random = new Random(1234);
+ spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ TraceOptions.builder().setIsSampled(true).build());
+ notSampledSpanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ TraceOptions.DEFAULT);
+ spanOptions = EnumSet.of(Span.Options.RECORD_EVENTS);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void newSpan_WithNullContext() {
+ new NoopSpan(null, null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void newSpan_SampledContextAndNullOptions() {
+ new NoopSpan(spanContext, null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void newSpan_SampledContextAndEmptyOptions() {
+ new NoopSpan(spanContext, EnumSet.noneOf(Span.Options.class));
+ }
+
+ @Test
+ public void getOptions_WhenNullOptions() {
+ Span span = new NoopSpan(notSampledSpanContext, null);
+ assertThat(span.getOptions()).isEmpty();
+ }
+
+ @Test
+ public void getContextAndOptions() {
+ Span span = new NoopSpan(spanContext, spanOptions);
+ assertThat(span.getContext()).isEqualTo(spanContext);
+ assertThat(span.getOptions()).isEqualTo(spanOptions);
+ }
+
+ @Test
+ public void putAttributeCallsAddAttributesByDefault() {
+ Span span = Mockito.spy(new NoopSpan(spanContext, spanOptions));
+ span.putAttribute("MyKey", AttributeValue.booleanAttributeValue(true));
+ span.end();
+ verify(span)
+ .putAttributes(
+ eq(Collections.singletonMap("MyKey", AttributeValue.booleanAttributeValue(true))));
+ }
+
+ @Test
+ public void endCallsEndWithDefaultOptions() {
+ Span span = Mockito.spy(new NoopSpan(spanContext, spanOptions));
+ span.end();
+ verify(span).end(same(EndSpanOptions.DEFAULT));
+ }
+
+ @Test
+ public void addMessageEventDefaultImplementation() {
+ Span mockSpan = Mockito.mock(Span.class);
+ MessageEvent messageEvent =
+ MessageEvent.builder(MessageEvent.Type.SENT, 123)
+ .setUncompressedMessageSize(456)
+ .setCompressedMessageSize(789)
+ .build();
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.SENT, 123)
+ .setUncompressedMessageSize(456)
+ .setCompressedMessageSize(789)
+ .build();
+ Mockito.doCallRealMethod().when(mockSpan).addMessageEvent(messageEvent);
+ mockSpan.addMessageEvent(messageEvent);
+ verify(mockSpan).addNetworkEvent(eq(networkEvent));
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/StatusTest.java b/api/src/test/java/io/opencensus/trace/StatusTest.java
new file mode 100644
index 00000000..108db2d2
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/StatusTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Status}. */
+@RunWith(JUnit4.class)
+public class StatusTest {
+ @Test
+ public void status_Ok() {
+ assertThat(Status.OK.getCanonicalCode()).isEqualTo(Status.CanonicalCode.OK);
+ assertThat(Status.OK.getDescription()).isNull();
+ assertThat(Status.OK.isOk()).isTrue();
+ }
+
+ @Test
+ public void createStatus_WithDescription() {
+ Status status = Status.UNKNOWN.withDescription("This is an error.");
+ assertThat(status.getCanonicalCode()).isEqualTo(Status.CanonicalCode.UNKNOWN);
+ assertThat(status.getDescription()).isEqualTo("This is an error.");
+ assertThat(status.isOk()).isFalse();
+ }
+
+ @Test
+ public void status_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(Status.OK, Status.OK.withDescription(null));
+ tester.addEqualityGroup(
+ Status.CANCELLED.withDescription("ThisIsAnError"),
+ Status.CANCELLED.withDescription("ThisIsAnError"));
+ tester.addEqualityGroup(Status.UNKNOWN.withDescription("This is an error."));
+ tester.testEquals();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TraceComponentTest.java b/api/src/test/java/io/opencensus/trace/TraceComponentTest.java
new file mode 100644
index 00000000..1c3f07d5
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TraceComponentTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.internal.ZeroTimeClock;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceComponent}. */
+@RunWith(JUnit4.class)
+public class TraceComponentTest {
+ @Test
+ public void defaultTracer() {
+ assertThat(TraceComponent.newNoopTraceComponent().getTracer()).isSameAs(Tracer.getNoopTracer());
+ }
+
+ @Test
+ public void defaultBinaryPropagationHandler() {
+ assertThat(TraceComponent.newNoopTraceComponent().getPropagationComponent())
+ .isSameAs(PropagationComponent.getNoopPropagationComponent());
+ }
+
+ @Test
+ public void defaultClock() {
+ assertThat(TraceComponent.newNoopTraceComponent().getClock()).isInstanceOf(ZeroTimeClock.class);
+ }
+
+ @Test
+ public void defaultTraceExporter() {
+ assertThat(TraceComponent.newNoopTraceComponent().getExportComponent())
+ .isInstanceOf(ExportComponent.newNoopExportComponent().getClass());
+ }
+
+ @Test
+ public void defaultTraceConfig() {
+ assertThat(TraceComponent.newNoopTraceComponent().getTraceConfig())
+ .isSameAs(TraceConfig.getNoopTraceConfig());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TraceIdTest.java b/api/src/test/java/io/opencensus/trace/TraceIdTest.java
new file mode 100644
index 00000000..c8b5dc8f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TraceIdTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceId}. */
+@RunWith(JUnit4.class)
+public class TraceIdTest {
+ private static final byte[] firstBytes =
+ new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'a'};
+ private static final byte[] secondBytes =
+ new byte[] {(byte) 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'A'};
+ private static final TraceId first = TraceId.fromBytes(firstBytes);
+ private static final TraceId second = TraceId.fromBytes(secondBytes);
+
+ @Test
+ public void invalidTraceId() {
+ assertThat(TraceId.INVALID.getBytes()).isEqualTo(new byte[16]);
+ }
+
+ @Test
+ public void isValid() {
+ assertThat(TraceId.INVALID.isValid()).isFalse();
+ assertThat(first.isValid()).isTrue();
+ assertThat(second.isValid()).isTrue();
+ }
+
+ @Test
+ public void getBytes() {
+ assertThat(first.getBytes()).isEqualTo(firstBytes);
+ assertThat(second.getBytes()).isEqualTo(secondBytes);
+ }
+
+ @Test
+ public void fromLowerBase16() {
+ assertThat(TraceId.fromLowerBase16("00000000000000000000000000000000"))
+ .isEqualTo(TraceId.INVALID);
+ assertThat(TraceId.fromLowerBase16("00000000000000000000000000000061")).isEqualTo(first);
+ assertThat(TraceId.fromLowerBase16("ff000000000000000000000000000041")).isEqualTo(second);
+ }
+
+ @Test
+ public void toLowerBase16() {
+ assertThat(TraceId.INVALID.toLowerBase16()).isEqualTo("00000000000000000000000000000000");
+ assertThat(first.toLowerBase16()).isEqualTo("00000000000000000000000000000061");
+ assertThat(second.toLowerBase16()).isEqualTo("ff000000000000000000000000000041");
+ }
+
+ @Test
+ public void traceId_CompareTo() {
+ assertThat(first.compareTo(second)).isGreaterThan(0);
+ assertThat(second.compareTo(first)).isLessThan(0);
+ assertThat(first.compareTo(TraceId.fromBytes(firstBytes))).isEqualTo(0);
+ }
+
+ @Test
+ public void traceId_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(TraceId.INVALID, TraceId.INVALID);
+ tester.addEqualityGroup(first, TraceId.fromBytes(Arrays.copyOf(firstBytes, firstBytes.length)));
+ tester.addEqualityGroup(
+ second, TraceId.fromBytes(Arrays.copyOf(secondBytes, secondBytes.length)));
+ tester.testEquals();
+ }
+
+ @Test
+ public void traceId_ToString() {
+ assertThat(TraceId.INVALID.toString()).contains("00000000000000000000000000000000");
+ assertThat(first.toString()).contains("00000000000000000000000000000061");
+ assertThat(second.toString()).contains("ff000000000000000000000000000041");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TraceOptionsTest.java b/api/src/test/java/io/opencensus/trace/TraceOptionsTest.java
new file mode 100644
index 00000000..3c46d097
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TraceOptionsTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceOptions}. */
+@RunWith(JUnit4.class)
+public class TraceOptionsTest {
+ private static final byte FIRST_BYTE = (byte) 0xff;
+ private static final byte SECOND_BYTE = 1;
+ private static final byte THIRD_BYTE = 6;
+
+ @Test
+ public void getOptions() {
+ assertThat(TraceOptions.DEFAULT.getOptions()).isEqualTo(0);
+ assertThat(TraceOptions.builder().setIsSampled(false).build().getOptions()).isEqualTo(0);
+ assertThat(TraceOptions.builder().setIsSampled(true).build().getOptions()).isEqualTo(1);
+ assertThat(TraceOptions.builder().setIsSampled(true).setIsSampled(false).build().getOptions())
+ .isEqualTo(0);
+ assertThat(TraceOptions.fromByte(FIRST_BYTE).getOptions()).isEqualTo(-1);
+ assertThat(TraceOptions.fromByte(SECOND_BYTE).getOptions()).isEqualTo(1);
+ assertThat(TraceOptions.fromByte(THIRD_BYTE).getOptions()).isEqualTo(6);
+ }
+
+ @Test
+ public void isSampled() {
+ assertThat(TraceOptions.DEFAULT.isSampled()).isFalse();
+ assertThat(TraceOptions.builder().setIsSampled(true).build().isSampled()).isTrue();
+ }
+
+ @Test
+ public void toFromByte() {
+ assertThat(TraceOptions.fromByte(FIRST_BYTE).getByte()).isEqualTo(FIRST_BYTE);
+ assertThat(TraceOptions.fromByte(SECOND_BYTE).getByte()).isEqualTo(SECOND_BYTE);
+ assertThat(TraceOptions.fromByte(THIRD_BYTE).getByte()).isEqualTo(THIRD_BYTE);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void deprecated_fromBytes() {
+ assertThat(TraceOptions.fromBytes(new byte[] {FIRST_BYTE}).getByte()).isEqualTo(FIRST_BYTE);
+ assertThat(TraceOptions.fromBytes(new byte[] {1, FIRST_BYTE}, 1).getByte())
+ .isEqualTo(FIRST_BYTE);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void deprecated_getBytes() {
+ assertThat(TraceOptions.fromByte(FIRST_BYTE).getBytes()).isEqualTo(new byte[] {FIRST_BYTE});
+ }
+
+ @Test
+ public void builder_FromOptions() {
+ assertThat(
+ TraceOptions.builder(TraceOptions.fromByte(THIRD_BYTE))
+ .setIsSampled(true)
+ .build()
+ .getOptions())
+ .isEqualTo(6 | 1);
+ }
+
+ @Test
+ public void traceOptions_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(TraceOptions.DEFAULT);
+ tester.addEqualityGroup(
+ TraceOptions.fromByte(SECOND_BYTE), TraceOptions.builder().setIsSampled(true).build());
+ tester.addEqualityGroup(TraceOptions.fromByte(FIRST_BYTE));
+ tester.testEquals();
+ }
+
+ @Test
+ public void traceOptions_ToString() {
+ assertThat(TraceOptions.DEFAULT.toString()).contains("sampled=false");
+ assertThat(TraceOptions.builder().setIsSampled(true).build().toString())
+ .contains("sampled=true");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TracerTest.java b/api/src/test/java/io/opencensus/trace/TracerTest.java
new file mode 100644
index 00000000..58dd4bbe
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TracerTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.common.Scope;
+import java.util.concurrent.Callable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link Tracer}. */
+@RunWith(JUnit4.class)
+// Need to suppress warnings for MustBeClosed because Java-6 does not support try-with-resources.
+@SuppressWarnings("MustBeClosedChecker")
+public class TracerTest {
+ private static final Tracer noopTracer = Tracer.getNoopTracer();
+ private static final String SPAN_NAME = "MySpanName";
+ @Mock private Tracer tracer;
+ @Mock private SpanBuilder spanBuilder;
+ @Mock private Span span;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void defaultGetCurrentSpan() {
+ assertThat(noopTracer.getCurrentSpan()).isEqualTo(BlankSpan.INSTANCE);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void withSpan_NullSpan() {
+ noopTracer.withSpan(null);
+ }
+
+ @Test
+ public void getCurrentSpan_WithSpan() {
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ Scope ws = noopTracer.withSpan(span);
+ try {
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(span);
+ } finally {
+ ws.close();
+ }
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test
+ public void wrapRunnable() {
+ Runnable runnable;
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ runnable =
+ tracer.withSpan(
+ span,
+ new Runnable() {
+ @Override
+ public void run() {
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(span);
+ }
+ });
+ // When we run the runnable we will have the span in the current Context.
+ runnable.run();
+ verifyZeroInteractions(span);
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test
+ public void wrapCallable() throws Exception {
+ final Object ret = new Object();
+ Callable<Object> callable;
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ callable =
+ tracer.withSpan(
+ span,
+ new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(span);
+ return ret;
+ }
+ });
+ // When we call the callable we will have the span in the current Context.
+ assertThat(callable.call()).isEqualTo(ret);
+ verifyZeroInteractions(span);
+ assertThat(noopTracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void spanBuilderWithName_NullName() {
+ noopTracer.spanBuilder(null);
+ }
+
+ @Test
+ public void defaultSpanBuilderWithName() {
+ assertThat(noopTracer.spanBuilder(SPAN_NAME).startSpan()).isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void spanBuilderWithParentAndName_NullName() {
+ noopTracer.spanBuilderWithExplicitParent(null, null);
+ }
+
+ @Test
+ public void defaultSpanBuilderWithParentAndName() {
+ assertThat(noopTracer.spanBuilderWithExplicitParent(SPAN_NAME, null).startSpan())
+ .isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void spanBuilderWithRemoteParent_NullName() {
+ noopTracer.spanBuilderWithRemoteParent(null, null);
+ }
+
+ @Test
+ public void defaultSpanBuilderWithRemoteParent_NullParent() {
+ assertThat(noopTracer.spanBuilderWithRemoteParent(SPAN_NAME, null).startSpan())
+ .isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test
+ public void defaultSpanBuilderWithRemoteParent() {
+ assertThat(noopTracer.spanBuilderWithRemoteParent(SPAN_NAME, SpanContext.INVALID).startSpan())
+ .isSameAs(BlankSpan.INSTANCE);
+ }
+
+ @Test
+ public void startSpanWithParentFromContext() {
+ Scope ws = tracer.withSpan(span);
+ try {
+ assertThat(tracer.getCurrentSpan()).isSameAs(span);
+ when(tracer.spanBuilderWithExplicitParent(same(SPAN_NAME), same(span)))
+ .thenReturn(spanBuilder);
+ assertThat(tracer.spanBuilder(SPAN_NAME)).isSameAs(spanBuilder);
+ } finally {
+ ws.close();
+ }
+ }
+
+ @Test
+ public void startSpanWithInvalidParentFromContext() {
+ Scope ws = tracer.withSpan(BlankSpan.INSTANCE);
+ try {
+ assertThat(tracer.getCurrentSpan()).isSameAs(BlankSpan.INSTANCE);
+ when(tracer.spanBuilderWithExplicitParent(same(SPAN_NAME), same(BlankSpan.INSTANCE)))
+ .thenReturn(spanBuilder);
+ assertThat(tracer.spanBuilder(SPAN_NAME)).isSameAs(spanBuilder);
+ } finally {
+ ws.close();
+ }
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TracestateTest.java b/api/src/test/java/io/opencensus/trace/TracestateTest.java
new file mode 100644
index 00000000..3374eb75
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TracestateTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.trace.Tracestate.Entry;
+import java.util.Arrays;
+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 Tracestate}. */
+@RunWith(JUnit4.class)
+public class TracestateTest {
+ private static final String FIRST_KEY = "key_1";
+ private static final String SECOND_KEY = "key_2";
+ private static final String FIRST_VALUE = "value.1";
+ private static final String SECOND_VALUE = "value.2";
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final Tracestate EMPTY = Tracestate.builder().build();
+ private final Tracestate firstTracestate = EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).build();
+ private final Tracestate secondTracestate =
+ EMPTY.toBuilder().set(SECOND_KEY, SECOND_VALUE).build();
+ private final Tracestate multiValueTracestate =
+ EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).set(SECOND_KEY, SECOND_VALUE).build();
+
+ @Test
+ public void get() {
+ assertThat(firstTracestate.get(FIRST_KEY)).isEqualTo(FIRST_VALUE);
+ assertThat(secondTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+ assertThat(multiValueTracestate.get(FIRST_KEY)).isEqualTo(FIRST_VALUE);
+ assertThat(multiValueTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+ }
+
+ @Test
+ public void getEntries() {
+ assertThat(firstTracestate.getEntries()).containsExactly(Entry.create(FIRST_KEY, FIRST_VALUE));
+ assertThat(secondTracestate.getEntries())
+ .containsExactly(Entry.create(SECOND_KEY, SECOND_VALUE));
+ assertThat(multiValueTracestate.getEntries())
+ .containsExactly(
+ Entry.create(FIRST_KEY, FIRST_VALUE), Entry.create(SECOND_KEY, SECOND_VALUE));
+ }
+
+ @Test
+ public void disallowsNullKey() {
+ thrown.expect(NullPointerException.class);
+ EMPTY.toBuilder().set(null, FIRST_VALUE).build();
+ }
+
+ @Test
+ public void invalidFirstKeyCharacter() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set("1_key", FIRST_VALUE).build();
+ }
+
+ @Test
+ public void invalidKeyCharacters() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set("kEy_1", FIRST_VALUE).build();
+ }
+
+ @Test
+ public void invalidKeySize() {
+ char[] chars = new char[257];
+ Arrays.fill(chars, 'a');
+ String longKey = new String(chars);
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(longKey, FIRST_VALUE).build();
+ }
+
+ @Test
+ public void allAllowedKeyCharacters() {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (char c = 'a'; c <= 'z'; c++) {
+ stringBuilder.append(c);
+ }
+ for (char c = '0'; c <= '9'; c++) {
+ stringBuilder.append(c);
+ }
+ stringBuilder.append('_');
+ stringBuilder.append('-');
+ stringBuilder.append('*');
+ stringBuilder.append('/');
+ String allowedKey = stringBuilder.toString();
+ assertThat(EMPTY.toBuilder().set(allowedKey, FIRST_VALUE).build().get(allowedKey))
+ .isEqualTo(FIRST_VALUE);
+ }
+
+ @Test
+ public void disallowsNullValue() {
+ thrown.expect(NullPointerException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, null).build();
+ }
+
+ @Test
+ public void valueCannotContainEqual() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, "my_vakue=5").build();
+ }
+
+ @Test
+ public void valueCannotContainComma() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, "first,second").build();
+ }
+
+ @Test
+ public void valueCannotContainTrailingSpaces() {
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, "first ").build();
+ }
+
+ @Test
+ public void invalidValueSize() {
+ char[] chars = new char[257];
+ Arrays.fill(chars, 'a');
+ String longValue = new String(chars);
+ thrown.expect(IllegalArgumentException.class);
+ EMPTY.toBuilder().set(FIRST_KEY, longValue).build();
+ }
+
+ @Test
+ public void allAllowedValueCharacters() {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (char c = ' ' /* '\u0020' */; c <= '~' /* '\u007E' */; c++) {
+ if (c == ',' || c == '=') {
+ continue;
+ }
+ stringBuilder.append(c);
+ }
+ String allowedValue = stringBuilder.toString();
+ assertThat(EMPTY.toBuilder().set(FIRST_KEY, allowedValue).build().get(FIRST_KEY))
+ .isEqualTo(allowedValue);
+ }
+
+ @Test
+ public void addEntry() {
+ assertThat(firstTracestate.toBuilder().set(SECOND_KEY, SECOND_VALUE).build())
+ .isEqualTo(multiValueTracestate);
+ }
+
+ @Test
+ public void updateEntry() {
+ assertThat(firstTracestate.toBuilder().set(FIRST_KEY, SECOND_VALUE).build().get(FIRST_KEY))
+ .isEqualTo(SECOND_VALUE);
+ Tracestate updatedMultiValueTracestate =
+ multiValueTracestate.toBuilder().set(FIRST_KEY, SECOND_VALUE).build();
+ assertThat(updatedMultiValueTracestate.get(FIRST_KEY)).isEqualTo(SECOND_VALUE);
+ assertThat(updatedMultiValueTracestate.get(SECOND_KEY)).isEqualTo(SECOND_VALUE);
+ }
+
+ @Test
+ public void addAndUpdateEntry() {
+ assertThat(
+ firstTracestate
+ .toBuilder()
+ .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+ .set(SECOND_KEY, FIRST_VALUE) // add a new entry
+ .build()
+ .getEntries())
+ .containsExactly(
+ Entry.create(FIRST_KEY, SECOND_VALUE), Entry.create(SECOND_KEY, FIRST_VALUE));
+ }
+
+ @Test
+ public void addSameKey() {
+ assertThat(
+ EMPTY
+ .toBuilder()
+ .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+ .set(FIRST_KEY, FIRST_VALUE) // add a new entry
+ .build()
+ .getEntries())
+ .containsExactly(Entry.create(FIRST_KEY, FIRST_VALUE));
+ }
+
+ @Test
+ public void remove() {
+ assertThat(multiValueTracestate.toBuilder().remove(SECOND_KEY).build())
+ .isEqualTo(firstTracestate);
+ }
+
+ @Test
+ public void addAndRemoveEntry() {
+ assertThat(
+ EMPTY
+ .toBuilder()
+ .set(FIRST_KEY, SECOND_VALUE) // update the existing entry
+ .remove(FIRST_KEY) // add a new entry
+ .build())
+ .isEqualTo(EMPTY);
+ }
+
+ @Test
+ public void remove_NullNotAllowed() {
+ thrown.expect(NullPointerException.class);
+ multiValueTracestate.toBuilder().remove(null).build();
+ }
+
+ @Test
+ public void tracestate_EqualsAndHashCode() {
+ EqualsTester tester = new EqualsTester();
+ tester.addEqualityGroup(EMPTY, EMPTY);
+ tester.addEqualityGroup(firstTracestate, EMPTY.toBuilder().set(FIRST_KEY, FIRST_VALUE).build());
+ tester.addEqualityGroup(
+ secondTracestate, EMPTY.toBuilder().set(SECOND_KEY, SECOND_VALUE).build());
+ tester.testEquals();
+ }
+
+ @Test
+ public void tracestate_ToString() {
+ assertThat(EMPTY.toString()).isEqualTo("Tracestate{entries=[]}");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/TracingTest.java b/api/src/test/java/io/opencensus/trace/TracingTest.java
new file mode 100644
index 00000000..e7c93a95
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/TracingTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+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 Tracing}. */
+@RunWith(JUnit4.class)
+public class TracingTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void loadTraceComponent_UsesProvidedClassLoader() {
+ final RuntimeException toThrow = new RuntimeException("UseClassLoader");
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("UseClassLoader");
+ Tracing.loadTraceComponent(
+ new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) {
+ throw toThrow;
+ }
+ });
+ }
+
+ @Test
+ public void loadTraceComponent_IgnoresMissingClasses() {
+ ClassLoader classLoader =
+ new ClassLoader() {
+ @Override
+ public Class<?> loadClass(String name) throws ClassNotFoundException {
+ throw new ClassNotFoundException();
+ }
+ };
+ assertThat(Tracing.loadTraceComponent(classLoader).getClass().getName())
+ .isEqualTo("io.opencensus.trace.TraceComponent$NoopTraceComponent");
+ }
+
+ @Test
+ public void defaultTracer() {
+ assertThat(Tracing.getTracer()).isSameAs(Tracer.getNoopTracer());
+ }
+
+ @Test
+ public void defaultBinaryPropagationHandler() {
+ assertThat(Tracing.getPropagationComponent())
+ .isSameAs(PropagationComponent.getNoopPropagationComponent());
+ }
+
+ @Test
+ public void defaultTraceExporter() {
+ assertThat(Tracing.getExportComponent())
+ .isInstanceOf(ExportComponent.newNoopExportComponent().getClass());
+ }
+
+ @Test
+ public void defaultTraceConfig() {
+ assertThat(Tracing.getTraceConfig()).isSameAs(TraceConfig.getNoopTraceConfig());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/config/TraceConfigTest.java b/api/src/test/java/io/opencensus/trace/config/TraceConfigTest.java
new file mode 100644
index 00000000..d48e0894
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/config/TraceConfigTest.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.trace.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.samplers.Samplers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceConfig}. */
+@RunWith(JUnit4.class)
+public class TraceConfigTest {
+ TraceConfig traceConfig = TraceConfig.getNoopTraceConfig();
+
+ @Test
+ public void activeTraceParams_NoOpImplementation() {
+ assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+ }
+
+ @Test
+ public void updateActiveTraceParams_NoOpImplementation() {
+ TraceParams traceParams =
+ TraceParams.DEFAULT
+ .toBuilder()
+ .setSampler(Samplers.alwaysSample())
+ .setMaxNumberOfAttributes(8)
+ .setMaxNumberOfAnnotations(9)
+ .setMaxNumberOfNetworkEvents(10)
+ .setMaxNumberOfMessageEvents(10)
+ .setMaxNumberOfLinks(11)
+ .build();
+ traceConfig.updateActiveTraceParams(traceParams);
+ assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/config/TraceParamsTest.java b/api/src/test/java/io/opencensus/trace/config/TraceParamsTest.java
new file mode 100644
index 00000000..bdf07d53
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/config/TraceParamsTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.trace.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.samplers.Samplers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceParams}. */
+@RunWith(JUnit4.class)
+public class TraceParamsTest {
+ @Test
+ public void defaultTraceParams() {
+ assertThat(TraceParams.DEFAULT.getSampler()).isEqualTo(Samplers.probabilitySampler(1e-4));
+ assertThat(TraceParams.DEFAULT.getMaxNumberOfAttributes()).isEqualTo(32);
+ assertThat(TraceParams.DEFAULT.getMaxNumberOfAnnotations()).isEqualTo(32);
+ assertThat(TraceParams.DEFAULT.getMaxNumberOfNetworkEvents()).isEqualTo(128);
+ assertThat(TraceParams.DEFAULT.getMaxNumberOfMessageEvents()).isEqualTo(128);
+ assertThat(TraceParams.DEFAULT.getMaxNumberOfLinks()).isEqualTo(32);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void updateTraceParams_NullSampler() {
+ TraceParams.DEFAULT.toBuilder().setSampler(null).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void updateTraceParams_NonPositiveMaxNumberOfAttributes() {
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfAttributes(0).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void updateTraceParams_NonPositiveMaxNumberOfAnnotations() {
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfAnnotations(0).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void updateTraceParams_NonPositiveMaxNumberOfNetworkEvents() {
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfNetworkEvents(0).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void updateTraceParams_NonPositiveMaxNumberOfMessageEvents() {
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfMessageEvents(0).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void updateTraceParams_NonPositiveMaxNumberOfLinks() {
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfLinks(0).build();
+ }
+
+ @Test
+ public void updateTraceParams_All() {
+ TraceParams traceParams =
+ TraceParams.DEFAULT
+ .toBuilder()
+ .setSampler(Samplers.alwaysSample())
+ .setMaxNumberOfAttributes(8)
+ .setMaxNumberOfAnnotations(9)
+ .setMaxNumberOfMessageEvents(10)
+ .setMaxNumberOfLinks(11)
+ .build();
+ assertThat(traceParams.getSampler()).isEqualTo(Samplers.alwaysSample());
+ assertThat(traceParams.getMaxNumberOfAttributes()).isEqualTo(8);
+ assertThat(traceParams.getMaxNumberOfAnnotations()).isEqualTo(9);
+ // test maxNumberOfNetworkEvent can be set via maxNumberOfMessageEvent
+ assertThat(traceParams.getMaxNumberOfNetworkEvents()).isEqualTo(10);
+ assertThat(traceParams.getMaxNumberOfMessageEvents()).isEqualTo(10);
+ assertThat(traceParams.getMaxNumberOfLinks()).isEqualTo(11);
+ }
+
+ @Test
+ public void updateTraceParams_maxNumberOfNetworkEvents() {
+ TraceParams traceParams =
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfNetworkEvents(10).build();
+ assertThat(traceParams.getMaxNumberOfNetworkEvents()).isEqualTo(10);
+ // test maxNumberOfMessageEvent can be set via maxNumberOfNetworkEvent
+ assertThat(traceParams.getMaxNumberOfMessageEvents()).isEqualTo(10);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/export/ExportComponentTest.java b/api/src/test/java/io/opencensus/trace/export/ExportComponentTest.java
new file mode 100644
index 00000000..d7f385d0
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/export/ExportComponentTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExportComponent}. */
+@RunWith(JUnit4.class)
+public class ExportComponentTest {
+ private final ExportComponent exportComponent = ExportComponent.newNoopExportComponent();
+
+ @Test
+ public void implementationOfSpanExporter() {
+ assertThat(exportComponent.getSpanExporter()).isEqualTo(SpanExporter.getNoopSpanExporter());
+ }
+
+ @Test
+ public void implementationOfActiveSpans() {
+ assertThat(exportComponent.getRunningSpanStore())
+ .isEqualTo(RunningSpanStore.getNoopRunningSpanStore());
+ }
+
+ @Test
+ public void implementationOfSampledSpanStore() {
+ assertThat(exportComponent.getSampledSpanStore())
+ .isInstanceOf(SampledSpanStore.newNoopSampledSpanStore().getClass());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/export/NoopRunningSpanStoreTest.java b/api/src/test/java/io/opencensus/trace/export/NoopRunningSpanStoreTest.java
new file mode 100644
index 00000000..960da27c
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/export/NoopRunningSpanStoreTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Collection;
+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 NoopRunningSpanStore}. */
+@RunWith(JUnit4.class)
+public final class NoopRunningSpanStoreTest {
+
+ private final RunningSpanStore runningSpanStore =
+ ExportComponent.newNoopExportComponent().getRunningSpanStore();
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void noopRunningSpanStore_GetSummary() {
+ RunningSpanStore.Summary summary = runningSpanStore.getSummary();
+ assertThat(summary.getPerSpanNameSummary()).isEmpty();
+ }
+
+ @Test
+ public void noopRunningSpanStore_GetRunningSpans_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ runningSpanStore.getRunningSpans(null);
+ }
+
+ @Test
+ public void noopRunningSpanStore_GetRunningSpans() {
+ Collection<SpanData> runningSpans =
+ runningSpanStore.getRunningSpans(RunningSpanStore.Filter.create("TestSpan", 0));
+ assertThat(runningSpans).isEmpty();
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/export/NoopSampledSpanStoreTest.java b/api/src/test/java/io/opencensus/trace/export/NoopSampledSpanStoreTest.java
new file mode 100644
index 00000000..6e9c7b0f
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/export/NoopSampledSpanStoreTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.opencensus.trace.Status.CanonicalCode;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NoopSampledSpanStore}. */
+@RunWith(JUnit4.class)
+public final class NoopSampledSpanStoreTest {
+
+ private static final SampledSpanStore.PerSpanNameSummary EMPTY_PER_SPAN_NAME_SUMMARY =
+ SampledSpanStore.PerSpanNameSummary.create(
+ Collections.<SampledSpanStore.LatencyBucketBoundaries, Integer>emptyMap(),
+ Collections.<CanonicalCode, Integer>emptyMap());
+
+ @Test
+ public void noopSampledSpanStore_RegisterUnregisterAndGetSummary() {
+ // should return empty before register
+ SampledSpanStore sampledSpanStore =
+ ExportComponent.newNoopExportComponent().getSampledSpanStore();
+ SampledSpanStore.Summary summary = sampledSpanStore.getSummary();
+ assertThat(summary.getPerSpanNameSummary()).isEmpty();
+
+ // should return non-empty summaries with zero latency/error sampled spans after register
+ sampledSpanStore.registerSpanNamesForCollection(
+ Collections.unmodifiableList(Lists.newArrayList("TestSpan1", "TestSpan2", "TestSpan3")));
+ summary = sampledSpanStore.getSummary();
+ assertThat(summary.getPerSpanNameSummary())
+ .containsExactly(
+ "TestSpan1", EMPTY_PER_SPAN_NAME_SUMMARY,
+ "TestSpan2", EMPTY_PER_SPAN_NAME_SUMMARY,
+ "TestSpan3", EMPTY_PER_SPAN_NAME_SUMMARY);
+
+ // should unregister specific spanNames
+ sampledSpanStore.unregisterSpanNamesForCollection(
+ Collections.unmodifiableList(Lists.newArrayList("TestSpan1", "TestSpan3")));
+ summary = sampledSpanStore.getSummary();
+ assertThat(summary.getPerSpanNameSummary())
+ .containsExactly("TestSpan2", EMPTY_PER_SPAN_NAME_SUMMARY);
+ }
+
+ @Test
+ public void noopSampledSpanStore_GetLatencySampledSpans() {
+ SampledSpanStore sampledSpanStore =
+ ExportComponent.newNoopExportComponent().getSampledSpanStore();
+ Collection<SpanData> latencySampledSpans =
+ sampledSpanStore.getLatencySampledSpans(
+ SampledSpanStore.LatencyFilter.create("TestLatencyFilter", 0, 0, 0));
+ assertThat(latencySampledSpans).isEmpty();
+ }
+
+ @Test
+ public void noopSampledSpanStore_GetErrorSampledSpans() {
+ SampledSpanStore sampledSpanStore =
+ ExportComponent.newNoopExportComponent().getSampledSpanStore();
+ Collection<SpanData> errorSampledSpans =
+ sampledSpanStore.getErrorSampledSpans(
+ SampledSpanStore.ErrorFilter.create("TestErrorFilter", null, 0));
+ assertThat(errorSampledSpans).isEmpty();
+ }
+
+ @Test
+ public void noopSampledSpanStore_GetRegisteredSpanNamesForCollection() {
+ SampledSpanStore sampledSpanStore =
+ ExportComponent.newNoopExportComponent().getSampledSpanStore();
+ sampledSpanStore.registerSpanNamesForCollection(
+ Collections.unmodifiableList(Lists.newArrayList("TestSpan3", "TestSpan4")));
+ Set<String> registeredSpanNames = sampledSpanStore.getRegisteredSpanNamesForCollection();
+ assertThat(registeredSpanNames).containsExactly("TestSpan3", "TestSpan4");
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/export/SpanDataTest.java b/api/src/test/java/io/opencensus/trace/export/SpanDataTest.java
new file mode 100644
index 00000000..b991d145
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/export/SpanDataTest.java
@@ -0,0 +1,321 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Link.Type;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.NetworkEvent;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData.Attributes;
+import io.opencensus.trace.export.SpanData.Links;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SpanData}. */
+@RunWith(JUnit4.class)
+public class SpanDataTest {
+ private static final Timestamp startTimestamp = Timestamp.create(123, 456);
+ private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457);
+ private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458);
+ private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459);
+ private static final Timestamp endTimestamp = Timestamp.create(123, 460);
+ private static final String SPAN_NAME = "MySpanName";
+ private static final String ANNOTATION_TEXT = "MyAnnotationText";
+ private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT);
+ private static final NetworkEvent recvNetworkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.RECV, 1).build();
+ private static final NetworkEvent sentNetworkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.SENT, 1).build();
+ private static final MessageEvent recvMessageEvent =
+ MessageEvent.builder(MessageEvent.Type.RECEIVED, 1).build();
+ private static final MessageEvent sentMessageEvent =
+ MessageEvent.builder(MessageEvent.Type.SENT, 1).build();
+ private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow");
+ private static final int CHILD_SPAN_COUNT = 13;
+ private final Random random = new Random(1234);
+ private final SpanContext spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+ private final SpanId parentSpanId = SpanId.generateRandomId(random);
+ private final Map<String, AttributeValue> attributesMap = new HashMap<String, AttributeValue>();
+ private final List<TimedEvent<Annotation>> annotationsList =
+ new ArrayList<TimedEvent<Annotation>>();
+ private final List<TimedEvent<NetworkEvent>> networkEventsList =
+ new ArrayList<SpanData.TimedEvent<NetworkEvent>>();
+ private final List<TimedEvent<MessageEvent>> messageEventsList =
+ new ArrayList<SpanData.TimedEvent<MessageEvent>>();
+ private final List<Link> linksList = new ArrayList<Link>();
+
+ private Attributes attributes;
+ private TimedEvents<Annotation> annotations;
+ private TimedEvents<NetworkEvent> networkEvents;
+ private TimedEvents<MessageEvent> messageEvents;
+ private Links links;
+
+ @Before
+ public void setUp() {
+ attributesMap.put("MyAttributeKey1", AttributeValue.longAttributeValue(10));
+ attributesMap.put("MyAttributeKey2", AttributeValue.booleanAttributeValue(true));
+ attributes = Attributes.create(attributesMap, 1);
+ annotationsList.add(SpanData.TimedEvent.create(eventTimestamp1, annotation));
+ annotationsList.add(SpanData.TimedEvent.create(eventTimestamp3, annotation));
+ annotations = TimedEvents.create(annotationsList, 2);
+ networkEventsList.add(SpanData.TimedEvent.create(eventTimestamp1, recvNetworkEvent));
+ networkEventsList.add(SpanData.TimedEvent.create(eventTimestamp2, sentNetworkEvent));
+ networkEvents = TimedEvents.create(networkEventsList, 3);
+ messageEventsList.add(SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent));
+ messageEventsList.add(SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent));
+ messageEvents = TimedEvents.create(messageEventsList, 3);
+ linksList.add(Link.fromSpanContext(spanContext, Type.CHILD_LINKED_SPAN));
+ links = Links.create(linksList, 0);
+ }
+
+ @Test
+ public void spanData_AllValues() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ true,
+ SPAN_NAME,
+ Kind.SERVER,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(spanData.getContext()).isEqualTo(spanContext);
+ assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+ assertThat(spanData.getHasRemoteParent()).isTrue();
+ assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+ assertThat(spanData.getKind()).isEqualTo(Kind.SERVER);
+ assertThat(spanData.getStartTimestamp()).isEqualTo(startTimestamp);
+ assertThat(spanData.getAttributes()).isEqualTo(attributes);
+ assertThat(spanData.getAnnotations()).isEqualTo(annotations);
+ assertThat(spanData.getNetworkEvents()).isEqualTo(networkEvents);
+ assertThat(spanData.getMessageEvents()).isEqualTo(messageEvents);
+ assertThat(spanData.getLinks()).isEqualTo(links);
+ assertThat(spanData.getChildSpanCount()).isEqualTo(CHILD_SPAN_COUNT);
+ assertThat(spanData.getStatus()).isEqualTo(status);
+ assertThat(spanData.getEndTimestamp()).isEqualTo(endTimestamp);
+ }
+
+ @Test
+ public void spanData_Create_Compatibility() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ true,
+ SPAN_NAME,
+ null,
+ startTimestamp,
+ attributes,
+ annotations,
+ networkEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(spanData.getContext()).isEqualTo(spanContext);
+ assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+ assertThat(spanData.getHasRemoteParent()).isTrue();
+ assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+ assertThat(spanData.getStartTimestamp()).isEqualTo(startTimestamp);
+ assertThat(spanData.getAttributes()).isEqualTo(attributes);
+ assertThat(spanData.getAnnotations()).isEqualTo(annotations);
+ assertThat(spanData.getNetworkEvents()).isEqualTo(networkEvents);
+ assertThat(spanData.getMessageEvents()).isEqualTo(messageEvents);
+ assertThat(spanData.getLinks()).isEqualTo(links);
+ assertThat(spanData.getChildSpanCount()).isEqualTo(CHILD_SPAN_COUNT);
+ assertThat(spanData.getStatus()).isEqualTo(status);
+ assertThat(spanData.getEndTimestamp()).isEqualTo(endTimestamp);
+ }
+
+ @Test
+ public void spanData_RootActiveSpan() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ null,
+ null,
+ SPAN_NAME,
+ null,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ null,
+ null,
+ null);
+ assertThat(spanData.getContext()).isEqualTo(spanContext);
+ assertThat(spanData.getParentSpanId()).isNull();
+ assertThat(spanData.getHasRemoteParent()).isNull();
+ assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+ assertThat(spanData.getStartTimestamp()).isEqualTo(startTimestamp);
+ assertThat(spanData.getAttributes()).isEqualTo(attributes);
+ assertThat(spanData.getAnnotations()).isEqualTo(annotations);
+ assertThat(spanData.getNetworkEvents()).isEqualTo(networkEvents);
+ assertThat(spanData.getMessageEvents()).isEqualTo(messageEvents);
+ assertThat(spanData.getLinks()).isEqualTo(links);
+ assertThat(spanData.getChildSpanCount()).isNull();
+ assertThat(spanData.getStatus()).isNull();
+ assertThat(spanData.getEndTimestamp()).isNull();
+ }
+
+ @Test
+ public void spanData_AllDataEmpty() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ false,
+ SPAN_NAME,
+ null,
+ startTimestamp,
+ Attributes.create(Collections.<String, AttributeValue>emptyMap(), 0),
+ TimedEvents.create(Collections.<SpanData.TimedEvent<Annotation>>emptyList(), 0),
+ TimedEvents.create(Collections.<SpanData.TimedEvent<MessageEvent>>emptyList(), 0),
+ Links.create(Collections.<Link>emptyList(), 0),
+ 0,
+ status,
+ endTimestamp);
+ assertThat(spanData.getContext()).isEqualTo(spanContext);
+ assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+ assertThat(spanData.getHasRemoteParent()).isFalse();
+ assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+ assertThat(spanData.getStartTimestamp()).isEqualTo(startTimestamp);
+ assertThat(spanData.getAttributes().getAttributeMap().isEmpty()).isTrue();
+ assertThat(spanData.getAnnotations().getEvents().isEmpty()).isTrue();
+ assertThat(spanData.getNetworkEvents().getEvents().isEmpty()).isTrue();
+ assertThat(spanData.getMessageEvents().getEvents().isEmpty()).isTrue();
+ assertThat(spanData.getLinks().getLinks().isEmpty()).isTrue();
+ assertThat(spanData.getChildSpanCount()).isEqualTo(0);
+ assertThat(spanData.getStatus()).isEqualTo(status);
+ assertThat(spanData.getEndTimestamp()).isEqualTo(endTimestamp);
+ }
+
+ @Test
+ public void spanDataEquals() {
+ SpanData allSpanData1 =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ false,
+ SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ SpanData allSpanData2 =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ false,
+ SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ SpanData emptySpanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ false,
+ SPAN_NAME,
+ null,
+ startTimestamp,
+ Attributes.create(Collections.<String, AttributeValue>emptyMap(), 0),
+ TimedEvents.create(Collections.<SpanData.TimedEvent<Annotation>>emptyList(), 0),
+ TimedEvents.create(Collections.<SpanData.TimedEvent<MessageEvent>>emptyList(), 0),
+ Links.create(Collections.<Link>emptyList(), 0),
+ 0,
+ status,
+ endTimestamp);
+ new EqualsTester()
+ .addEqualityGroup(allSpanData1, allSpanData2)
+ .addEqualityGroup(emptySpanData)
+ .testEquals();
+ }
+
+ @Test
+ public void spanData_ToString() {
+ String spanDataString =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ false,
+ SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp)
+ .toString();
+ assertThat(spanDataString).contains(spanContext.toString());
+ assertThat(spanDataString).contains(parentSpanId.toString());
+ assertThat(spanDataString).contains(SPAN_NAME);
+ assertThat(spanDataString).contains(Kind.CLIENT.toString());
+ assertThat(spanDataString).contains(startTimestamp.toString());
+ assertThat(spanDataString).contains(attributes.toString());
+ assertThat(spanDataString).contains(annotations.toString());
+ assertThat(spanDataString).contains(messageEvents.toString());
+ assertThat(spanDataString).contains(links.toString());
+ assertThat(spanDataString).contains(status.toString());
+ assertThat(spanDataString).contains(endTimestamp.toString());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/internal/BaseMessageEventUtilsTest.java b/api/src/test/java/io/opencensus/trace/internal/BaseMessageEventUtilsTest.java
new file mode 100644
index 00000000..4f8c8508
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/internal/BaseMessageEventUtilsTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018, 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.trace.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.NetworkEvent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BaseMessageEventUtils}. */
+@RunWith(JUnit4.class)
+public class BaseMessageEventUtilsTest {
+ private static final long SENT_EVENT_ID = 12345L;
+ private static final long RECV_EVENT_ID = 67890L;
+ private static final long UNCOMPRESSED_SIZE = 100;
+ private static final long COMPRESSED_SIZE = 99;
+
+ private static final MessageEvent SENT_MESSAGE_EVENT =
+ MessageEvent.builder(MessageEvent.Type.SENT, SENT_EVENT_ID)
+ .setUncompressedMessageSize(UNCOMPRESSED_SIZE)
+ .setCompressedMessageSize(COMPRESSED_SIZE)
+ .build();
+ private static final MessageEvent RECV_MESSAGE_EVENT =
+ MessageEvent.builder(MessageEvent.Type.RECEIVED, RECV_EVENT_ID)
+ .setUncompressedMessageSize(UNCOMPRESSED_SIZE)
+ .setCompressedMessageSize(COMPRESSED_SIZE)
+ .build();
+ private static final NetworkEvent SENT_NETWORK_EVENT =
+ NetworkEvent.builder(NetworkEvent.Type.SENT, SENT_EVENT_ID)
+ .setUncompressedMessageSize(UNCOMPRESSED_SIZE)
+ .setCompressedMessageSize(COMPRESSED_SIZE)
+ .build();
+ private static final NetworkEvent RECV_NETWORK_EVENT =
+ NetworkEvent.builder(NetworkEvent.Type.RECV, RECV_EVENT_ID)
+ .setUncompressedMessageSize(UNCOMPRESSED_SIZE)
+ .setCompressedMessageSize(COMPRESSED_SIZE)
+ .build();
+
+ @Test
+ public void networkEventToMessageEvent() {
+ assertThat(BaseMessageEventUtils.asMessageEvent(SENT_NETWORK_EVENT))
+ .isEqualTo(SENT_MESSAGE_EVENT);
+ assertThat(BaseMessageEventUtils.asMessageEvent(RECV_NETWORK_EVENT))
+ .isEqualTo(RECV_MESSAGE_EVENT);
+ }
+
+ @Test
+ public void messageEventToNetworkEvent() {
+ assertThat(BaseMessageEventUtils.asNetworkEvent(SENT_MESSAGE_EVENT))
+ .isEqualTo(SENT_NETWORK_EVENT);
+ assertThat(BaseMessageEventUtils.asNetworkEvent(RECV_MESSAGE_EVENT))
+ .isEqualTo(RECV_NETWORK_EVENT);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/propagation/BinaryFormatTest.java b/api/src/test/java/io/opencensus/trace/propagation/BinaryFormatTest.java
new file mode 100644
index 00000000..64544ffe
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/propagation/BinaryFormatTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.SpanContext;
+import java.text.ParseException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link BinaryFormat}. */
+@RunWith(JUnit4.class)
+public class BinaryFormatTest {
+ private static final BinaryFormat binaryFormat = BinaryFormat.getNoopBinaryFormat();
+
+ @Test(expected = NullPointerException.class)
+ public void toBinaryValue_NullSpanContext() {
+ binaryFormat.toBinaryValue(null);
+ }
+
+ @Test
+ public void toBinaryValue_NotNullSpanContext() {
+ assertThat(binaryFormat.toBinaryValue(SpanContext.INVALID)).isEqualTo(new byte[0]);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void toByteArray_NullSpanContext() {
+ binaryFormat.toByteArray(null);
+ }
+
+ @Test
+ public void toByteArray_NotNullSpanContext() {
+ assertThat(binaryFormat.toByteArray(SpanContext.INVALID)).isEqualTo(new byte[0]);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void fromBinaryValue_NullInput() throws ParseException {
+ binaryFormat.fromBinaryValue(null);
+ }
+
+ @Test
+ public void fromBinaryValue_NotNullInput() throws ParseException {
+ assertThat(binaryFormat.fromBinaryValue(new byte[0])).isEqualTo(SpanContext.INVALID);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void fromByteArray_NullInput() throws SpanContextParseException {
+ binaryFormat.fromByteArray(null);
+ }
+
+ @Test
+ public void fromByteArray_NotNullInput() throws SpanContextParseException {
+ assertThat(binaryFormat.fromByteArray(new byte[0])).isEqualTo(SpanContext.INVALID);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/propagation/PropagationComponentTest.java b/api/src/test/java/io/opencensus/trace/propagation/PropagationComponentTest.java
new file mode 100644
index 00000000..ba64e98e
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/propagation/PropagationComponentTest.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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PropagationComponent}. */
+@RunWith(JUnit4.class)
+public class PropagationComponentTest {
+ private final PropagationComponent propagationComponent =
+ PropagationComponent.getNoopPropagationComponent();
+
+ @Test
+ public void implementationOfBinaryFormat() {
+ assertThat(propagationComponent.getBinaryFormat())
+ .isEqualTo(BinaryFormat.getNoopBinaryFormat());
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/propagation/SpanContextParseExceptionTest.java b/api/src/test/java/io/opencensus/trace/propagation/SpanContextParseExceptionTest.java
new file mode 100644
index 00000000..92efb35d
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/propagation/SpanContextParseExceptionTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SpanContextParseException}. */
+@RunWith(JUnit4.class)
+public class SpanContextParseExceptionTest {
+
+ @Test
+ public void createWithMessage() {
+ assertThat(new SpanContextParseException("my message").getMessage()).isEqualTo("my message");
+ }
+
+ @Test
+ public void createWithMessageAndCause() {
+ IOException cause = new IOException();
+ SpanContextParseException parseException = new SpanContextParseException("my message", cause);
+ assertThat(parseException.getMessage()).isEqualTo("my message");
+ assertThat(parseException.getCause()).isEqualTo(cause);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/propagation/TextFormatTest.java b/api/src/test/java/io/opencensus/trace/propagation/TextFormatTest.java
new file mode 100644
index 00000000..c2e6e127
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/propagation/TextFormatTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TextFormat}. */
+@RunWith(JUnit4.class)
+public class TextFormatTest {
+ private static final TextFormat textFormat = TextFormat.getNoopTextFormat();
+
+ @Test(expected = NullPointerException.class)
+ public void inject_NullSpanContext() {
+ textFormat.inject(
+ null,
+ new Object(),
+ new Setter<Object>() {
+ @Override
+ public void put(Object carrier, String key, String value) {}
+ });
+ }
+
+ @Test
+ public void inject_NotNullSpanContext_DoesNotFail() {
+ textFormat.inject(
+ SpanContext.INVALID,
+ new Object(),
+ new Setter<Object>() {
+ @Override
+ public void put(Object carrier, String key, String value) {}
+ });
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void fromHeaders_NullGetter() throws SpanContextParseException {
+ textFormat.extract(new Object(), null);
+ }
+
+ @Test
+ public void fromHeaders_NotNullGetter() throws SpanContextParseException {
+ assertThat(
+ textFormat.extract(
+ new Object(),
+ new Getter<Object>() {
+ @Nullable
+ @Override
+ public String get(Object carrier, String key) {
+ return null;
+ }
+ }))
+ .isSameAs(SpanContext.INVALID);
+ }
+}
diff --git a/api/src/test/java/io/opencensus/trace/samplers/SamplersTest.java b/api/src/test/java/io/opencensus/trace/samplers/SamplersTest.java
new file mode 100644
index 00000000..7a46e97a
--- /dev/null
+++ b/api/src/test/java/io/opencensus/trace/samplers/SamplersTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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.trace.samplers;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.NoopSpan;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Random;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link Samplers}. */
+@RunWith(JUnit4.class)
+public class SamplersTest {
+ private static final String SPAN_NAME = "MySpanName";
+ private static final int NUM_SAMPLE_TRIES = 1000;
+ private final Random random = new Random(1234);
+ private final TraceId traceId = TraceId.generateRandomId(random);
+ private final SpanId parentSpanId = SpanId.generateRandomId(random);
+ private final SpanId spanId = SpanId.generateRandomId(random);
+ private final SpanContext sampledSpanContext =
+ SpanContext.create(traceId, parentSpanId, TraceOptions.builder().setIsSampled(true).build());
+ private final SpanContext notSampledSpanContext =
+ SpanContext.create(traceId, parentSpanId, TraceOptions.DEFAULT);
+ private final Span sampledSpan =
+ new NoopSpan(sampledSpanContext, EnumSet.of(Span.Options.RECORD_EVENTS));
+
+ @Test
+ public void alwaysSampleSampler_AlwaysReturnTrue() {
+ // Sampled parent.
+ assertThat(
+ Samplers.alwaysSample()
+ .shouldSample(
+ sampledSpanContext,
+ false,
+ traceId,
+ spanId,
+ "Another name",
+ Collections.<Span>emptyList()))
+ .isTrue();
+ // Not sampled parent.
+ assertThat(
+ Samplers.alwaysSample()
+ .shouldSample(
+ notSampledSpanContext,
+ false,
+ traceId,
+ spanId,
+ "Yet another name",
+ Collections.<Span>emptyList()))
+ .isTrue();
+ }
+
+ @Test
+ public void alwaysSampleSampler_ToString() {
+ assertThat(Samplers.alwaysSample().toString()).isEqualTo("AlwaysSampleSampler");
+ }
+
+ @Test
+ public void neverSampleSampler_AlwaysReturnFalse() {
+ // Sampled parent.
+ assertThat(
+ Samplers.neverSample()
+ .shouldSample(
+ sampledSpanContext,
+ false,
+ traceId,
+ spanId,
+ "bar",
+ Collections.<Span>emptyList()))
+ .isFalse();
+ // Not sampled parent.
+ assertThat(
+ Samplers.neverSample()
+ .shouldSample(
+ notSampledSpanContext,
+ false,
+ traceId,
+ spanId,
+ "quux",
+ Collections.<Span>emptyList()))
+ .isFalse();
+ }
+
+ @Test
+ public void neverSampleSampler_ToString() {
+ assertThat(Samplers.neverSample().toString()).isEqualTo("NeverSampleSampler");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void probabilitySampler_outOfRangeHighProbability() {
+ Samplers.probabilitySampler(1.01);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void probabilitySampler_outOfRangeLowProbability() {
+ Samplers.probabilitySampler(-0.00001);
+ }
+
+ // Applies the given sampler to NUM_SAMPLE_TRIES random traceId/spanId pairs.
+ private static void assertSamplerSamplesWithProbability(
+ Sampler sampler, SpanContext parent, List<Span> parentLinks, double probability) {
+ Random random = new Random(1234);
+ int count = 0; // Count of spans with sampling enabled
+ for (int i = 0; i < NUM_SAMPLE_TRIES; i++) {
+ if (sampler.shouldSample(
+ parent,
+ false,
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ SPAN_NAME,
+ parentLinks)) {
+ count++;
+ }
+ }
+ double proportionSampled = (double) count / NUM_SAMPLE_TRIES;
+ // Allow for a large amount of slop (+/- 10%) in number of sampled traces, to avoid flakiness.
+ assertThat(proportionSampled < probability + 0.1 && proportionSampled > probability - 0.1)
+ .isTrue();
+ }
+
+ @Test
+ public void probabilitySampler_DifferentProbabilities_NotSampledParent() {
+ final Sampler neverSample = Samplers.probabilitySampler(0.0);
+ assertSamplerSamplesWithProbability(
+ neverSample, notSampledSpanContext, Collections.<Span>emptyList(), 0.0);
+ final Sampler alwaysSample = Samplers.probabilitySampler(1.0);
+ assertSamplerSamplesWithProbability(
+ alwaysSample, notSampledSpanContext, Collections.<Span>emptyList(), 1.0);
+ final Sampler fiftyPercentSample = Samplers.probabilitySampler(0.5);
+ assertSamplerSamplesWithProbability(
+ fiftyPercentSample, notSampledSpanContext, Collections.<Span>emptyList(), 0.5);
+ final Sampler twentyPercentSample = Samplers.probabilitySampler(0.2);
+ assertSamplerSamplesWithProbability(
+ twentyPercentSample, notSampledSpanContext, Collections.<Span>emptyList(), 0.2);
+ final Sampler twoThirdsSample = Samplers.probabilitySampler(2.0 / 3.0);
+ assertSamplerSamplesWithProbability(
+ twoThirdsSample, notSampledSpanContext, Collections.<Span>emptyList(), 2.0 / 3.0);
+ }
+
+ @Test
+ public void probabilitySampler_DifferentProbabilities_SampledParent() {
+ final Sampler neverSample = Samplers.probabilitySampler(0.0);
+ assertSamplerSamplesWithProbability(
+ neverSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+ final Sampler alwaysSample = Samplers.probabilitySampler(1.0);
+ assertSamplerSamplesWithProbability(
+ alwaysSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+ final Sampler fiftyPercentSample = Samplers.probabilitySampler(0.5);
+ assertSamplerSamplesWithProbability(
+ fiftyPercentSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+ final Sampler twentyPercentSample = Samplers.probabilitySampler(0.2);
+ assertSamplerSamplesWithProbability(
+ twentyPercentSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+ final Sampler twoThirdsSample = Samplers.probabilitySampler(2.0 / 3.0);
+ assertSamplerSamplesWithProbability(
+ twoThirdsSample, sampledSpanContext, Collections.<Span>emptyList(), 1.0);
+ }
+
+ @Test
+ public void probabilitySampler_DifferentProbabilities_SampledParentLink() {
+ final Sampler neverSample = Samplers.probabilitySampler(0.0);
+ assertSamplerSamplesWithProbability(
+ neverSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+ final Sampler alwaysSample = Samplers.probabilitySampler(1.0);
+ assertSamplerSamplesWithProbability(
+ alwaysSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+ final Sampler fiftyPercentSample = Samplers.probabilitySampler(0.5);
+ assertSamplerSamplesWithProbability(
+ fiftyPercentSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+ final Sampler twentyPercentSample = Samplers.probabilitySampler(0.2);
+ assertSamplerSamplesWithProbability(
+ twentyPercentSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+ final Sampler twoThirdsSample = Samplers.probabilitySampler(2.0 / 3.0);
+ assertSamplerSamplesWithProbability(
+ twoThirdsSample, notSampledSpanContext, Arrays.asList(sampledSpan), 1.0);
+ }
+
+ @Test
+ public void probabilitySampler_SampleBasedOnTraceId() {
+ final Sampler defaultProbability = Samplers.probabilitySampler(0.0001);
+ // This traceId will not be sampled by the ProbabilitySampler because the first 8 bytes as long
+ // is not less than probability * Long.MAX_VALUE;
+ TraceId notSampledtraceId =
+ TraceId.fromBytes(
+ new byte[] {
+ (byte) 0x8F,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ });
+ assertThat(
+ defaultProbability.shouldSample(
+ null,
+ false,
+ notSampledtraceId,
+ SpanId.generateRandomId(random),
+ SPAN_NAME,
+ Collections.<Span>emptyList()))
+ .isFalse();
+ // This traceId will be sampled by the ProbabilitySampler because the first 8 bytes as long
+ // is less than probability * Long.MAX_VALUE;
+ TraceId sampledtraceId =
+ TraceId.fromBytes(
+ new byte[] {
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ });
+ assertThat(
+ defaultProbability.shouldSample(
+ null,
+ false,
+ sampledtraceId,
+ SpanId.generateRandomId(random),
+ SPAN_NAME,
+ Collections.<Span>emptyList()))
+ .isTrue();
+ }
+
+ @Test
+ public void probabilitySampler_getDescription() {
+ assertThat((Samplers.probabilitySampler(0.5)).getDescription())
+ .isEqualTo(String.format("ProbabilitySampler{%.6f}", 0.5));
+ }
+
+ @Test
+ public void probabilitySampler_ToString() {
+ assertThat((Samplers.probabilitySampler(0.5)).toString()).contains("0.5");
+ }
+}
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 00000000..34493a90
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,10 @@
+install:
+ - git submodule update --init --recursive
+
+build_script:
+ # The Gradle build script runs the integration tests of contrib/agent using different Java
+ # versions. %JAVA_HOMES% lists the home directories of the JDK installations used for
+ # integration testing. Also see https://www.appveyor.com/docs/build-environment/#java.
+ - set JAVA_HOMES=C:\Program Files\Java\jdk1.6.0\jre;C:\Program Files\Java\jdk1.7.0\jre;C:\Program Files\Java\jdk1.8.0\jre
+ - gradlew.bat clean assemble check --stacktrace
+ - pushd examples && gradlew.bat clean assemble check --stacktrace && popd
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 00000000..e591a8d6
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,3 @@
+# OpenCensus Benchmarks
+
+See [here](../CONTRIBUTING.md#benchmarks) for how to run and debug issues with benchmarks. \ No newline at end of file
diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle
new file mode 100644
index 00000000..04688dd3
--- /dev/null
+++ b/benchmarks/build.gradle
@@ -0,0 +1,18 @@
+description = 'OpenCensus Benchmarks'
+
+dependencies {
+ compile project(':opencensus-api'),
+ project(':opencensus-impl-core'),
+ project(':opencensus-impl-lite'),
+ project(':opencensus-impl')
+}
+
+jmhReport {
+ jmhResultPath = project.file("${project.buildDir}/reports/jmh/results.json")
+ jmhReportOutput = project.file("${project.buildDir}/reports/jmh")
+}
+
+tasks.jmh.finalizedBy tasks.jmhReport
+
+// Disable checkstyle if not java8.
+checkstyleJmh.enabled = JavaVersion.current().isJava8Compatible()
diff --git a/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/BenchmarksUtil.java b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/BenchmarksUtil.java
new file mode 100644
index 00000000..e917817a
--- /dev/null
+++ b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/BenchmarksUtil.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018, 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.benchmarks.trace;
+
+import io.opencensus.impllite.trace.TraceComponentImplLite;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+
+/** Util class for Benchmarks. */
+final class BenchmarksUtil {
+ private static final TraceComponentImplLite traceComponentImplLite = new TraceComponentImplLite();
+
+ static Tracer getTracer(String implementation) {
+ if (implementation.equals("impl")) {
+ // We can return the global tracer here because if impl is linked the global tracer will be
+ // the impl one.
+ // TODO(bdrutu): Make everything not be a singleton (disruptor, etc.) and use a new
+ // TraceComponentImpl similar to TraceComponentImplLite.
+ return Tracing.getTracer();
+ } else if (implementation.equals("impl-lite")) {
+ return traceComponentImplLite.getTracer();
+ } else {
+ throw new RuntimeException("Invalid tracer implementation requested.");
+ }
+ }
+
+ // Avoid instances of this class.
+ private BenchmarksUtil() {}
+}
diff --git a/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/RecordTraceEventsBenchmark.java b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/RecordTraceEventsBenchmark.java
new file mode 100644
index 00000000..992937a1
--- /dev/null
+++ b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/RecordTraceEventsBenchmark.java
@@ -0,0 +1,122 @@
+/*
+ * 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.benchmarks.trace;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+
+/** Benchmarks for {@link Span} to record trace events. */
+@State(Scope.Benchmark)
+public class RecordTraceEventsBenchmark {
+ private static final String SPAN_NAME = "MySpanName";
+ private static final String ANNOTATION_DESCRIPTION = "MyAnnotation";
+ private static final String ATTRIBUTE_KEY = "MyAttributeKey";
+ private static final String ATTRIBUTE_VALUE = "MyAttributeValue";
+
+ @State(Scope.Benchmark)
+ public static class Data {
+
+ private Span linkedSpan = BlankSpan.INSTANCE;
+ private Span span = BlankSpan.INSTANCE;
+
+ @Param({"impl", "impl-lite"})
+ String implementation;
+
+ @Param({"true", "false"})
+ boolean sampled;
+
+ @Setup
+ public void setup() {
+ Tracer tracer = BenchmarksUtil.getTracer(implementation);
+ linkedSpan =
+ tracer
+ .spanBuilderWithExplicitParent(SPAN_NAME, null)
+ .setSampler(sampled ? Samplers.alwaysSample() : Samplers.neverSample())
+ .startSpan();
+ span =
+ tracer
+ .spanBuilderWithExplicitParent(SPAN_NAME, null)
+ .setSampler(sampled ? Samplers.alwaysSample() : Samplers.neverSample())
+ .startSpan();
+ }
+
+ @TearDown
+ public void doTearDown() {
+ checkState(linkedSpan != BlankSpan.INSTANCE, "Uninitialized linkedSpan");
+ checkState(span != BlankSpan.INSTANCE, "Uninitialized span");
+ linkedSpan.end();
+ span.end();
+ }
+ }
+
+ /** This benchmark attempts to measure performance of adding an attribute to the span. */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span putAttribute(Data data) {
+ data.span.putAttribute(ATTRIBUTE_KEY, AttributeValue.stringAttributeValue(ATTRIBUTE_VALUE));
+ return data.span;
+ }
+
+ /** This benchmark attempts to measure performance of adding an annotation to the span. */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span addAnnotation(Data data) {
+ data.span.addAnnotation(ANNOTATION_DESCRIPTION);
+ return data.span;
+ }
+
+ /** This benchmark attempts to measure performance of adding a network event to the span. */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span addMessageEvent(Data data) {
+ data.span.addMessageEvent(
+ io.opencensus.trace.MessageEvent.builder(Type.RECEIVED, 1)
+ .setUncompressedMessageSize(3)
+ .build());
+ return data.span;
+ }
+
+ /** This benchmark attempts to measure performance of adding a link to the span. */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span addLink(Data data) {
+ data.span.addLink(
+ Link.fromSpanContext(data.linkedSpan.getContext(), Link.Type.PARENT_LINKED_SPAN));
+ return data.span;
+ }
+}
diff --git a/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/StartEndSpanBenchmark.java b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/StartEndSpanBenchmark.java
new file mode 100644
index 00000000..02f77f5f
--- /dev/null
+++ b/benchmarks/src/jmh/java/io/opencensus/benchmarks/trace/StartEndSpanBenchmark.java
@@ -0,0 +1,164 @@
+/*
+ * 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.benchmarks.trace;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+
+/** Benchmarks for {@link io.opencensus.trace.SpanBuilder} and {@link Span}. */
+@State(Scope.Benchmark)
+public class StartEndSpanBenchmark {
+ private static final String SPAN_NAME = "MySpanName";
+
+ @State(Scope.Benchmark)
+ public static class Data {
+ private Tracer tracer;
+ private Span rootSpan = BlankSpan.INSTANCE;
+
+ @Param({"impl", "impl-lite"})
+ String implementation;
+
+ @Setup
+ public void setup() {
+ tracer = BenchmarksUtil.getTracer(implementation);
+
+ rootSpan =
+ tracer
+ .spanBuilderWithExplicitParent(SPAN_NAME, null)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ }
+
+ @TearDown
+ public void doTearDown() {
+ checkState(rootSpan != BlankSpan.INSTANCE, "Uninitialized rootSpan");
+ rootSpan.end();
+ }
+ }
+
+ /**
+ * This benchmark attempts to measure performance of start/end for a non-sampled root {@code
+ * Span}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span startEndNonSampledRootSpan(Data data) {
+ Span span =
+ data.tracer
+ .spanBuilderWithExplicitParent(SPAN_NAME, null)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ span.end();
+ return span;
+ }
+
+ /**
+ * This benchmark attempts to measure performance of start/end for a root {@code Span} with record
+ * events option.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span startEndRecordEventsRootSpan(Data data) {
+ Span span =
+ data.tracer
+ .spanBuilderWithExplicitParent(SPAN_NAME, null)
+ .setSampler(Samplers.neverSample())
+ .setRecordEvents(true)
+ .startSpan();
+ span.end();
+ return span;
+ }
+
+ /**
+ * This benchmark attempts to measure performance of start/end for a sampled root {@code Span}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span startEndSampledRootSpan(Data data) {
+ Span span = data.tracer.spanBuilder(SPAN_NAME).setSampler(Samplers.alwaysSample()).startSpan();
+ span.end();
+ return span;
+ }
+
+ /**
+ * This benchmark attempts to measure performance of start/end for a non-sampled child {@code
+ * Span}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span startEndNonSampledChildSpan(Data data) {
+ Span span =
+ data.tracer
+ .spanBuilderWithExplicitParent(SPAN_NAME, data.rootSpan)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ span.end();
+ return span;
+ }
+
+ /**
+ * This benchmark attempts to measure performance of start/end for a child {@code Span} with
+ * record events option.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span startEndRecordEventsChildSpan(Data data) {
+ Span span =
+ data.tracer
+ .spanBuilderWithExplicitParent(SPAN_NAME, data.rootSpan)
+ .setSampler(Samplers.neverSample())
+ .setRecordEvents(true)
+ .startSpan();
+ span.end();
+ return span;
+ }
+
+ /**
+ * This benchmark attempts to measure performance of start/end for a sampled child {@code Span}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Span startEndSampledChildSpan(Data data) {
+ Span span =
+ data.tracer
+ .spanBuilderWithExplicitParent(SPAN_NAME, data.rootSpan)
+ .setSampler(Samplers.alwaysSample())
+ .startSpan();
+ span.end();
+ return span;
+ }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..dcb006ce
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,497 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ mavenLocal()
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath 'ru.vyarus:gradle-animalsniffer-plugin:1.4.6'
+ classpath 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.16'
+ classpath "net.ltgt.gradle:gradle-apt-plugin:0.18"
+ classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
+ classpath "gradle.plugin.com.github.sherter.google-java-format:google-java-format-gradle-plugin:0.7.1"
+ classpath "me.champeau.gradle:jmh-gradle-plugin:0.4.7"
+ classpath "gradle.plugin.io.morethan.jmhreport:gradle-jmh-report:0.7.0"
+ }
+}
+
+// Display the version report using: ./gradlew dependencyUpdates
+// Also see https://github.com/ben-manes/gradle-versions-plugin.
+apply plugin: 'com.github.ben-manes.versions'
+
+// Don't use the Checker Framework by default, since it interferes with Error Prone.
+def useCheckerFramework = rootProject.hasProperty('checkerFramework')
+def useErrorProne = !useCheckerFramework
+
+subprojects {
+ apply plugin: "checkstyle"
+ apply plugin: 'maven'
+ apply plugin: 'idea'
+ apply plugin: 'eclipse'
+ apply plugin: 'java'
+ apply plugin: "signing"
+ apply plugin: "jacoco"
+ // The plugin only has an effect if a signature is specified
+ apply plugin: 'ru.vyarus.animalsniffer'
+ apply plugin: 'findbugs'
+ apply plugin: 'net.ltgt.apt'
+ apply plugin: "me.champeau.gradle.jmh"
+ apply plugin: "io.morethan.jmhreport"
+ // Plugins that require java8
+ if (JavaVersion.current().isJava8Compatible()) {
+ if (useErrorProne) {
+ apply plugin: "net.ltgt.errorprone"
+ }
+ apply plugin: 'com.github.sherter.google-java-format'
+ }
+
+ group = "io.opencensus"
+ version = "0.17.0-SNAPSHOT" // CURRENT_OPENCENSUS_VERSION
+
+ sourceCompatibility = 1.6
+ targetCompatibility = 1.6
+
+ repositories {
+ mavenCentral()
+ mavenLocal()
+ }
+
+ if (useCheckerFramework) {
+ configurations {
+ checkerFrameworkJavac {
+ description = 'a customization of the Open JDK javac compiler with additional support for type annotations'
+ }
+ checkerFrameworkAnnotatedJDK {
+ description = 'a copy of JDK classes with Checker Framework type qualifiers inserted'
+ }
+ }
+ }
+
+ [compileJava, compileTestJava, compileJmhJava].each() {
+ // We suppress the "try" warning because it disallows managing an auto-closeable with
+ // try-with-resources without referencing the auto-closeable within the try block.
+ // We suppress the "processing" warning as suggested in
+ // https://groups.google.com/forum/#!topic/bazel-discuss/_R3A9TJSoPM
+ it.options.compilerArgs += ["-Xlint:all", "-Xlint:-try", "-Xlint:-processing"]
+ if (useErrorProne) {
+ if (JavaVersion.current().isJava8Compatible()) {
+ it.options.compilerArgs += ["-XepAllDisabledChecksAsWarnings", "-XepDisableWarningsInGeneratedCode"]
+
+ // MutableMethodReturnType can suggest returning Guava types from
+ // API methods (https://github.com/google/error-prone/issues/982).
+ it.options.compilerArgs += ["-Xep:MutableMethodReturnType:OFF"]
+
+ // ReturnMissingNullable conflicts with Checker Framework null analysis.
+ it.options.compilerArgs += ["-Xep:ReturnMissingNullable:OFF"]
+
+ // OpenCensus doesn't currently use Var annotations.
+ it.options.compilerArgs += ["-Xep:Var:OFF"]
+ }
+ }
+ if (useCheckerFramework) {
+ it.options.compilerArgs += [
+ '-processor',
+ 'com.google.auto.value.processor.AutoValueProcessor,org.checkerframework.checker.nullness.NullnessChecker',
+ "-Astubs=$rootDir/checker-framework/stubs"
+ ]
+ }
+ it.options.encoding = "UTF-8"
+ // Protobuf-generated code produces some warnings.
+ // https://github.com/google/protobuf/issues/2718
+ it.options.compilerArgs += ["-Xlint:-cast"]
+ if (!JavaVersion.current().isJava9()) {
+ // TODO(sebright): Enable -Werror for Java 9 once we upgrade AutoValue (issue #1017).
+ it.options.compilerArgs += ["-Werror"]
+ }
+ if (JavaVersion.current().isJava7()) {
+ // Suppress all deprecation warnings with Java 7, since there are some bugs in its handling of
+ // @SuppressWarnings. See
+ // https://stackoverflow.com/questions/26921774/how-to-avoid-deprecation-warnings-when-suppresswarningsdeprecation-doesnt
+ it.options.compilerArgs += ["-Xlint:-deprecation"]
+
+ // TODO(bdrutu): Enable for Java 7 when fix the issue with configuring bootstrap class.
+ // [options] bootstrap class path not set in conjunction with -source 1.6
+ it.options.compilerArgs += ["-Xlint:-options"]
+ }
+ if (JavaVersion.current().isJava9()) {
+ // TODO(sebright): Currently, building with Java 9 produces the following "options" warnings:
+ //
+ // :opencensus-api:compileJavawarning: [options] bootstrap class path not set in conjunction with -source 1.6
+ // warning: [options] source value 1.6 is obsolete and will be removed in a future release
+ // warning: [options] target value 1.6 is obsolete and will be removed in a future release
+ it.options.compilerArgs += ["-Xlint:-options"]
+ }
+ }
+
+ compileTestJava {
+ // serialVersionUID is basically guaranteed to be useless in tests
+ options.compilerArgs += ["-Xlint:-serial"]
+ // It undeprecates DoubleSubject.isEqualTo(Double).
+ options.compilerArgs += ["-Xlint:-deprecation"]
+ }
+
+ jar.manifest {
+ attributes('Implementation-Title': name,
+ 'Implementation-Version': version,
+ 'Built-By': System.getProperty('user.name'),
+ 'Built-JDK': System.getProperty('java.version'),
+ 'Source-Compatibility': sourceCompatibility,
+ 'Target-Compatibility': targetCompatibility)
+ }
+
+ javadoc.options {
+ encoding = 'UTF-8'
+ links 'https://docs.oracle.com/javase/8/docs/api/'
+ }
+
+ ext {
+ appengineVersion = '1.9.64'
+ aspectjVersion = '1.8.11'
+ autoValueVersion = '1.4'
+ findBugsAnnotationsVersion = '3.0.1'
+ findBugsJsr305Version = '3.0.2'
+ errorProneVersion = '2.3.1'
+ grpcVersion = '1.14.0'
+ guavaVersion = '20.0'
+ googleAuthVersion = '0.11.0'
+ googleCloudBetaVersion = '0.64.0-beta'
+ googleCloudGaVersion = '1.46.0'
+ log4j2Version = '2.11.1'
+ signalfxVersion = '0.0.39'
+ springBootVersion = '1.5.15.RELEASE'
+ springCloudVersion = '1.3.4.RELEASE'
+ springVersion = '4.3.12.RELEASE'
+ prometheusVersion = '0.4.0'
+ protobufVersion = '3.5.1'
+ zipkinReporterVersion = '2.3.2'
+ jaegerReporterVersion = '0.27.0'
+ opencensusProtoVersion = '0.0.2'
+ dropwizardVersion = '3.1.2'
+
+ libraries = [
+ appengine_api: "com.google.appengine:appengine-api-1.0-sdk:${appengineVersion}",
+ aspectj: "org.aspectj:aspectjrt:${aspectjVersion}",
+ auto_value: "com.google.auto.value:auto-value:${autoValueVersion}",
+ auto_service: 'com.google.auto.service:auto-service:1.0-rc3',
+ byte_buddy: 'net.bytebuddy:byte-buddy:1.7.11',
+ config: 'com.typesafe:config:1.2.1',
+ disruptor: 'com.lmax:disruptor:3.4.1',
+ errorprone: "com.google.errorprone:error_prone_annotations:${errorProneVersion}",
+ findbugs_annotations: "com.google.code.findbugs:annotations:${findBugsAnnotationsVersion}",
+ google_auth: "com.google.auth:google-auth-library-credentials:${googleAuthVersion}",
+ google_cloud_logging: "com.google.cloud:google-cloud-logging:${googleCloudGaVersion}",
+ google_cloud_trace: "com.google.cloud:google-cloud-trace:${googleCloudBetaVersion}",
+ log4j2: "org.apache.logging.log4j:log4j-core:${log4j2Version}",
+ zipkin_reporter: "io.zipkin.reporter2:zipkin-reporter:${zipkinReporterVersion}",
+ zipkin_urlconnection: "io.zipkin.reporter2:zipkin-sender-urlconnection:${zipkinReporterVersion}",
+ jaeger_reporter: "com.uber.jaeger:jaeger-core:${jaegerReporterVersion}",
+ google_cloud_monitoring: "com.google.cloud:google-cloud-monitoring:${googleCloudGaVersion}",
+ grpc_context: "io.grpc:grpc-context:${grpcVersion}",
+ grpc_core: "io.grpc:grpc-core:${grpcVersion}",
+ grpc_netty: "io.grpc:grpc-netty:${grpcVersion}",
+ grpc_stub: "io.grpc:grpc-stub:${grpcVersion}",
+ guava: "com.google.guava:guava:${guavaVersion}",
+ jsr305: "com.google.code.findbugs:jsr305:${findBugsJsr305Version}",
+ signalfx_java: "com.signalfx.public:signalfx-java:${signalfxVersion}",
+ spring_aspects: "org.springframework:spring-aspects:${springVersion}",
+ spring_boot_starter_web: "org.springframework.boot:spring-boot-starter-web:${springBootVersion}",
+ spring_cloud_build: "org.springframework.cloud:spring-cloud-build:${springCloudVersion}",
+ spring_cloud_starter_sleuth: "org.springframework.cloud:spring-cloud-starter-sleuth:${springCloudVersion}",
+ spring_context: "org.springframework:spring-context:${springVersion}",
+ spring_context_support: "org.springframework:spring-context-support:${springVersion}",
+ prometheus_simpleclient: "io.prometheus:simpleclient:${prometheusVersion}",
+ protobuf: "com.google.protobuf:protobuf-java:${protobufVersion}",
+ opencensus_proto: "io.opencensus:opencensus-proto:${opencensusProtoVersion}",
+
+ // Test dependencies.
+ guava_testlib: "com.google.guava:guava-testlib:${guavaVersion}",
+ junit: 'junit:junit:4.12',
+ mockito: 'org.mockito:mockito-core:1.9.5',
+ spring_test: "org.springframework:spring-test:${springVersion}",
+ truth: 'com.google.truth:truth:0.30',
+ dropwizard: "io.dropwizard.metrics:metrics-core:${dropwizardVersion}",
+ ]
+ }
+
+ configurations {
+ compile {
+ // Detect Maven Enforcer's dependencyConvergence failures. We only
+ // care for artifacts used as libraries by others.
+ if (!(project.name in ['benchmarks', 'opencensus-all',
+ 'opencensus-exporter-stats-stackdriver',
+ 'opencensus-exporter-trace-stackdriver',
+ 'opencensus-exporter-trace-jaeger'])) {
+ resolutionStrategy.failOnVersionConflict()
+ }
+ }
+ }
+
+ dependencies {
+ if (useCheckerFramework) {
+ ext.checkerFrameworkVersion = '2.5.5'
+
+ // 2.4.0 is the last version of the Checker Framework compiler that supports annotations
+ // in comments, though it should continue to work with newer versions of the Checker Framework.
+ // See
+ // https://github.com/census-instrumentation/opencensus-java/pull/1112#issuecomment-381366366.
+ ext.checkerFrameworkCompilerVersion = '2.4.0'
+
+ ext.jdkVersion = 'jdk8'
+ checkerFrameworkAnnotatedJDK "org.checkerframework:${jdkVersion}:${checkerFrameworkVersion}"
+ checkerFrameworkJavac "org.checkerframework:compiler:${checkerFrameworkCompilerVersion}"
+ compileOnly "org.checkerframework:checker:${checkerFrameworkVersion}"
+ compile "org.checkerframework:checker-qual:${checkerFrameworkVersion}"
+ compileOnly libraries.auto_value
+ }
+
+ compileOnly libraries.errorprone,
+ libraries.jsr305
+
+ testCompile libraries.guava_testlib,
+ libraries.junit,
+ libraries.mockito,
+ libraries.truth
+
+ if (useErrorProne && JavaVersion.current().isJava8Compatible()) {
+ // The ErrorProne plugin defaults to the latest, which would break our
+ // build if error prone releases a new version with a new check
+ errorprone "com.google.errorprone:error_prone_core:${errorProneVersion}"
+ }
+ }
+
+ findbugs {
+ toolVersion = findBugsAnnotationsVersion
+ ignoreFailures = false // bug free or it doesn't ship!
+ effort = 'max'
+ reportLevel = 'low' // low = sensitive to even minor mistakes
+ omitVisitors = [] // bugs that we want to ignore
+ excludeFilter = file("$rootDir/findbugs-exclude.xml")
+ }
+ // Generate html report for findbugs.
+ findbugsMain {
+ reports {
+ xml.enabled = false
+ html.enabled = true
+ }
+ }
+ findbugsTest {
+ reports {
+ xml.enabled = false
+ html.enabled = true
+ }
+ }
+ findbugsJmh {
+ reports {
+ xml.enabled = false
+ html.enabled = true
+ }
+ }
+
+ checkstyle {
+ configFile = file("$rootDir/buildscripts/checkstyle.xml")
+ toolVersion = "8.12"
+ ignoreFailures = false
+ if (rootProject.hasProperty("checkstyle.ignoreFailures")) {
+ ignoreFailures = rootProject.properties["checkstyle.ignoreFailures"].toBoolean()
+ }
+ configProperties["rootDir"] = rootDir
+ }
+
+ // Disable checkstyle if no java8.
+ checkstyleMain.enabled = JavaVersion.current().isJava8Compatible()
+ checkstyleTest.enabled = JavaVersion.current().isJava8Compatible()
+ checkstyleJmh.enabled = JavaVersion.current().isJava8Compatible()
+
+ // Google formatter works only on java8.
+ if (JavaVersion.current().isJava8Compatible()) {
+ googleJavaFormat {
+ toolVersion '1.6'
+ }
+
+ afterEvaluate { // Allow subproject to add more source sets.
+ tasks.googleJavaFormat {
+ source = sourceSets*.allJava
+ include '**/*.java'
+ }
+
+ tasks.verifyGoogleJavaFormat {
+ source = sourceSets*.allJava
+ include '**/*.java'
+ }
+ }
+ }
+
+ signing {
+ required false
+ sign configurations.archives
+ }
+
+ task javadocJar(type: Jar) {
+ classifier = 'javadoc'
+ from javadoc
+ }
+
+ task sourcesJar(type: Jar) {
+ classifier = 'sources'
+ from sourceSets.main.allSource
+ }
+
+ artifacts {
+ archives javadocJar, sourcesJar
+ }
+
+ jmh {
+ jmhVersion = '1.20'
+ warmupIterations = 10
+ iterations = 10
+ fork = 1
+ failOnError = true
+ resultFormat = 'JSON'
+ // Allow to run single benchmark class like:
+ // ./gradlew -PjmhIncludeSingleClass=StatsTraceContextBenchmark clean :grpc-core:jmh
+ if (project.hasProperty('jmhIncludeSingleClass')) {
+ include = [
+ project.property('jmhIncludeSingleClass')
+ ]
+ }
+ }
+
+ jmhReport {
+ jmhResultPath = project.file("${project.buildDir}/reports/jmh/results.json")
+ jmhReportOutput = project.file("${project.buildDir}/reports/jmh")
+ }
+
+ tasks.jmh.finalizedBy tasks.jmhReport
+
+ uploadArchives {
+ repositories {
+ mavenDeployer {
+ beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
+
+ def configureAuth = {
+ if (rootProject.hasProperty('ossrhUsername') && rootProject.hasProperty('ossrhPassword')) {
+ authentication(userName:rootProject.ossrhUsername, password: rootProject.ossrhPassword)
+ }
+ }
+
+ repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/", configureAuth)
+
+ snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/", configureAuth)
+
+ pom.project {
+ name "OpenCensus"
+ packaging 'jar'
+ description project.description
+ url 'https://github.com/census-instrumentation/opencensus-java'
+
+ scm {
+ connection 'scm:svn:https://github.com/census-instrumentation/opencensus-java'
+ developerConnection 'scm:git:git@github.com/census-instrumentation/opencensus-java'
+ url 'https://github.com/census-instrumentation/opencensus-java'
+ }
+
+ licenses {
+ license {
+ name 'The Apache License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ }
+ }
+
+ developers {
+ developer {
+ id 'io.opencensus'
+ name 'OpenCensus Contributors'
+ email 'census-developers@googlegroups.com'
+ url 'opencensus.io'
+ // https://issues.gradle.org/browse/GRADLE-2719
+ organization = 'OpenCensus Authors'
+ organizationUrl 'https://www.opencensus.io'
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Upload the following artifacts only:
+ uploadArchives.onlyIf {
+ name in ['opencensus-api',
+ 'opencensus-contrib-agent',
+ 'opencensus-contrib-appengine-standard-util',
+ 'opencensus-contrib-dropwizard',
+ 'opencensus-contrib-exemplar-util',
+ 'opencensus-contrib-grpc-metrics',
+ 'opencensus-contrib-grpc-util',
+ 'opencensus-contrib-http-util',
+ 'opencensus-contrib-log-correlation-log4j2',
+ 'opencensus-contrib-log-correlation-stackdriver',
+ 'opencensus-contrib-monitored-resource-util',
+ 'opencensus-contrib-spring',
+ 'opencensus-contrib-spring-sleuth-v1x',
+ 'opencensus-contrib-zpages',
+ 'opencensus-exporter-stats-prometheus',
+ 'opencensus-exporter-stats-signalfx',
+ 'opencensus-exporter-stats-stackdriver',
+ 'opencensus-exporter-trace-instana',
+ 'opencensus-exporter-trace-logging',
+ 'opencensus-exporter-trace-ocagent',
+ 'opencensus-exporter-trace-stackdriver',
+ 'opencensus-exporter-trace-zipkin',
+ 'opencensus-exporter-trace-jaeger',
+ 'opencensus-impl-core',
+ 'opencensus-impl-lite',
+ 'opencensus-impl',
+ 'opencensus-testing']
+ }
+
+ // At a test failure, log the stack trace to the console so that we don't
+ // have to open the HTML in a browser.
+ test {
+ testLogging {
+ exceptionFormat = 'full'
+ showExceptions true
+ showCauses true
+ showStackTraces true
+ }
+ maxHeapSize = '1500m'
+ }
+
+ if (useCheckerFramework) {
+ allprojects {
+ tasks.withType(JavaCompile).all { JavaCompile compile ->
+ compile.doFirst {
+ compile.options.compilerArgs += [
+ '-Xmaxerrs', '10000',
+ "-Xbootclasspath/p:${configurations.checkerFrameworkAnnotatedJDK.asPath}",
+ "-AskipDefs=\\.AutoValue_|^io.opencensus.contrib.appengine.standard.util.TraceIdProto\$|^io.opencensus.contrib.appengine.standard.util.TraceProto\$",
+ "-AinvariantArrays"
+ ]
+ options.fork = true
+ options.forkOptions.jvmArgs += ["-Xbootclasspath/p:${configurations.checkerFrameworkJavac.asPath}"]
+ }
+ }
+ }
+ }
+
+ // For projects that depend on gRPC during test execution, make sure to
+ // also configure ALPN if running on a platform (e.g. FreeBSD) that is not
+ // supported by io.netty:netty-tcnative-boringssl-static:jar. Also see:
+ // https://github.com/grpc/grpc-java/blob/master/SECURITY.md#tls-with-jdk-jetty-alpnnpn
+ if (project.name in ['opencensus-exporter-stats-stackdriver',
+ 'opencensus-exporter-trace-stackdriver']) {
+ def os = org.gradle.internal.os.OperatingSystem.current()
+ if (!os.isLinux() && !os.isWindows() && !os.isMacOsX()) {
+ configurations {
+ alpn
+ }
+ dependencies {
+ alpn 'org.mortbay.jetty.alpn:jetty-alpn-agent:2.0.7'
+ }
+ test {
+ jvmArgs "-javaagent:${configurations.alpn.asPath}"
+ }
+ }
+ }
+}
diff --git a/buildscripts/checkstyle.license b/buildscripts/checkstyle.license
new file mode 100644
index 00000000..27bac1a2
--- /dev/null
+++ b/buildscripts/checkstyle.license
@@ -0,0 +1,15 @@
+^/\*$
+^ \* Copyright \d\d\d\d(-\d\d)?, 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\.$
+^ \*/$ \ No newline at end of file
diff --git a/buildscripts/checkstyle.xml b/buildscripts/checkstyle.xml
new file mode 100644
index 00000000..50b146e7
--- /dev/null
+++ b/buildscripts/checkstyle.xml
@@ -0,0 +1,277 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+ "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
+ "https://checkstyle.org/dtds/configuration_1_3.dtd">
+
+<!--
+ Checkstyle configuration that checks the Google coding conventions from Google Java Style
+ that can be found at https://google.github.io/styleguide/javaguide.html.
+
+ Checkstyle is very configurable. Be sure to read the documentation at
+ http://checkstyle.sf.net (or in your downloaded distribution).
+
+ To completely disable a check, just comment it out or delete it from the file.
+
+ Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
+ -->
+
+<module name = "Checker">
+ <property name="charset" value="UTF-8"/>
+
+ <property name="severity" value="error"/>
+
+
+ <module name="RegexpHeader">
+ <property name="headerFile" value="${rootDir}/buildscripts/checkstyle.license"/>
+ <property name="fileExtensions" value="java"/>
+ </module>
+
+ <property name="fileExtensions" value="java, properties, xml"/>
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <module name="FileTabCharacter">
+ <property name="eachLine" value="true"/>
+ </module>
+
+ <module name="TreeWalker">
+ <module name="OuterTypeFilename"/>
+ <module name="IllegalTokenText">
+ <property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
+ <property name="format"
+ value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
+ <property name="message"
+ value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
+ </module>
+ <module name="AvoidEscapedUnicodeCharacters">
+ <property name="allowEscapesForControlCharacters" value="true"/>
+ <property name="allowByTailComment" value="true"/>
+ <property name="allowNonPrintableEscapes" value="true"/>
+ </module>
+ <module name="LineLength">
+ <property name="max" value="100"/>
+ <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
+ </module>
+ <module name="AvoidStarImport"/>
+ <module name="RedundantImport"/>
+ <module name="OneTopLevelClass"/>
+ <module name="NoLineWrap"/>
+ <module name="EmptyBlock">
+ <property name="option" value="TEXT"/>
+ <property name="tokens"
+ value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
+ </module>
+ <module name="NeedBraces"/>
+ <module name="LeftCurly"/>
+ <module name="RightCurly">
+ <property name="id" value="RightCurlySame"/>
+ <property name="tokens"
+ value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
+ LITERAL_DO"/>
+ </module>
+ <module name="RightCurly">
+ <property name="id" value="RightCurlyAlone"/>
+ <property name="option" value="alone"/>
+ <property name="tokens"
+ value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
+ INSTANCE_INIT"/>
+ </module>
+ <module name="WhitespaceAround">
+ <property name="allowEmptyConstructors" value="true"/>
+ <property name="allowEmptyMethods" value="true"/>
+ <property name="allowEmptyTypes" value="true"/>
+ <property name="allowEmptyLoops" value="true"/>
+ <message key="ws.notFollowed"
+ value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
+ <message key="ws.notPreceded"
+ value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
+ </module>
+ <module name="OneStatementPerLine"/>
+ <module name="MultipleVariableDeclarations"/>
+ <module name="ArrayTypeStyle"/>
+ <!-- <!-\- This rule conflicts with Error Prone's exhaustiveness checking. -\-> -->
+ <!-- <module name="MissingSwitchDefault"/> -->
+ <module name="FallThrough"/>
+ <module name="UpperEll"/>
+ <module name="ModifierOrder"/>
+ <module name="EmptyLineSeparator">
+ <property name="allowNoEmptyLineBetweenFields" value="true"/>
+ </module>
+ <module name="SeparatorWrap">
+ <property name="id" value="SeparatorWrapDot"/>
+ <property name="tokens" value="DOT"/>
+ <property name="option" value="nl"/>
+ </module>
+ <module name="SeparatorWrap">
+ <property name="id" value="SeparatorWrapComma"/>
+ <property name="tokens" value="COMMA"/>
+ <property name="option" value="EOL"/>
+ </module>
+ <module name="SeparatorWrap">
+ <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
+ <property name="id" value="SeparatorWrapEllipsis"/>
+ <property name="tokens" value="ELLIPSIS"/>
+ <property name="option" value="EOL"/>
+ </module>
+ <module name="SeparatorWrap">
+ <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
+ <property name="id" value="SeparatorWrapArrayDeclarator"/>
+ <property name="tokens" value="ARRAY_DECLARATOR"/>
+ <property name="option" value="EOL"/>
+ </module>
+ <module name="SeparatorWrap">
+ <property name="id" value="SeparatorWrapMethodRef"/>
+ <property name="tokens" value="METHOD_REF"/>
+ <property name="option" value="nl"/>
+ </module>
+ <module name="PackageName">
+ <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
+ <message key="name.invalidPattern"
+ value="Package name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="TypeName">
+ <message key="name.invalidPattern"
+ value="Type name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="MemberName">
+ <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
+ <message key="name.invalidPattern"
+ value="Member name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="ParameterName">
+ <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
+ <message key="name.invalidPattern"
+ value="Parameter name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="LambdaParameterName">
+ <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
+ <message key="name.invalidPattern"
+ value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="CatchParameterName">
+ <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
+ <message key="name.invalidPattern"
+ value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="LocalVariableName">
+ <property name="tokens" value="VARIABLE_DEF"/>
+ <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
+ <message key="name.invalidPattern"
+ value="Local variable name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="ClassTypeParameterName">
+ <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+ <message key="name.invalidPattern"
+ value="Class type name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="MethodTypeParameterName">
+ <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+ <message key="name.invalidPattern"
+ value="Method type name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="InterfaceTypeParameterName">
+ <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+ <message key="name.invalidPattern"
+ value="Interface type name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="NoFinalizer"/>
+ <module name="GenericWhitespace">
+ <message key="ws.followed"
+ value="GenericWhitespace ''{0}'' is followed by whitespace."/>
+ <message key="ws.preceded"
+ value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
+ <message key="ws.illegalFollow"
+ value="GenericWhitespace ''{0}'' should followed by whitespace."/>
+ <message key="ws.notPreceded"
+ value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
+ </module>
+ <!-- <!-\- Checkstyle indentation rules conflict with google-java-format: -\-> -->
+ <!-- <module name="Indentation"> -->
+ <!-- <property name="basicOffset" value="2"/> -->
+ <!-- <property name="braceAdjustment" value="0"/> -->
+ <!-- <property name="caseIndent" value="2"/> -->
+ <!-- <property name="throwsIndent" value="4"/> -->
+ <!-- <property name="lineWrappingIndentation" value="4"/> -->
+ <!-- <property name="arrayInitIndent" value="2"/> -->
+ <!-- </module> -->
+ <module name="AbbreviationAsWordInName">
+ <property name="ignoreFinal" value="false"/>
+ <property name="allowedAbbreviationLength" value="1"/>
+ </module>
+ <module name="OverloadMethodsDeclarationOrder"/>
+ <!-- <!-\- Many unit tests define all variables at the start of the method. -\-> -->
+ <!-- <module name="VariableDeclarationUsageDistance"/> -->
+ <module name="CustomImportOrder">
+ <property name="sortImportsInGroupAlphabetically" value="true"/>
+ <property name="separateLineBetweenGroups" value="true"/>
+ <property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
+ </module>
+ <module name="MethodParamPad"/>
+ <module name="NoWhitespaceBefore">
+ <property name="tokens"
+ value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
+ <property name="allowLineBreaks" value="true"/>
+ </module>
+ <module name="ParenPad"/>
+ <module name="OperatorWrap">
+ <property name="option" value="NL"/>
+ <property name="tokens"
+ value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
+ LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
+ </module>
+ <module name="AnnotationLocation">
+ <property name="id" value="AnnotationLocationMostCases"/>
+ <property name="tokens"
+ value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
+ </module>
+ <module name="AnnotationLocation">
+ <property name="id" value="AnnotationLocationVariables"/>
+ <property name="tokens" value="VARIABLE_DEF"/>
+ <property name="allowSamelineMultipleAnnotations" value="true"/>
+ </module>
+ <module name="NonEmptyAtclauseDescription"/>
+ <module name="JavadocTagContinuationIndentation"/>
+ <module name="SummaryJavadoc">
+ <property name="forbiddenSummaryFragments"
+ value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
+ </module>
+ <module name="JavadocParagraph"/>
+ <module name="AtclauseOrder">
+ <property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
+ <property name="target"
+ value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
+ </module>
+ <module name="JavadocMethod">
+ <property name="scope" value="public"/>
+ <property name="allowMissingParamTags" value="true"/>
+ <property name="allowMissingThrowsTags" value="true"/>
+ <property name="allowMissingReturnTag" value="true"/>
+ <property name="minLineCount" value="2"/>
+ <!-- <!-\- Too restrictive for tests -\-> -->
+ <!-- <property name="allowedAnnotations" value="Override, Test"/ -->
+ <property name="allowedAnnotations"
+ value="Override, Test, Before, After, BeforeClass, AfterClass, Setup,
+ TearDown"/>
+ <property name="allowThrowsTagsForSubclasses" value="true"/>
+ </module>
+ <module name="MethodName">
+ <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
+ <message key="name.invalidPattern"
+ value="Method name ''{0}'' must match pattern ''{1}''."/>
+ </module>
+ <module name="SingleLineJavadoc">
+ <!-- <!-\- Wrong interpretation of the style guide; -\-> -->
+ <!-- <property name="ignoreInlineTags" value="false"/ -->
+ </module>
+ <module name="EmptyCatchBlock">
+ <property name="exceptionVariableName" value="expected"/>
+ </module>
+ <module name="CommentsIndentation"/>
+ <module name="SuppressWarningsHolder"/>
+ <module name="ImportControl">
+ <property name="file" value="${rootDir}/buildscripts/import-control.xml"/>
+ <property name="path" value="^.*[\\/]src[\\/]main[\\/]java[\\/].*$"/>
+ </module>
+ <module name="SuppressionCommentFilter"/>
+ </module>
+ <module name="SuppressWarningsFilter"/>
+</module>
diff --git a/buildscripts/codecov.yml b/buildscripts/codecov.yml
new file mode 100644
index 00000000..a2c8d611
--- /dev/null
+++ b/buildscripts/codecov.yml
@@ -0,0 +1,2 @@
+ignore:
+ - "impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java" # ignore VarInt
diff --git a/buildscripts/import-control.xml b/buildscripts/import-control.xml
new file mode 100644
index 00000000..d545878a
--- /dev/null
+++ b/buildscripts/import-control.xml
@@ -0,0 +1,252 @@
+<?xml version="1.0"?>
+<!DOCTYPE import-control PUBLIC
+ "-//Puppy Crawl//DTD Import Control 1.3//EN"
+ "http://checkstyle.sourceforge.net/dtds/import_control_1_3.dtd">
+
+<!--
+
+General guidelines on imports:
+
+- 'stats' depends on 'tags', but 'tags' shouldn't depend on 'stats' or 'trace'.
+ 'stats'/'tags' and 'trace' should remain independent, where possible.
+
+- Packages should not be split between artifacts.
+
+- 'internal' packages should only be imported by packages within the same
+ artifact.
+
+- Since we are trying to remove dependencies on Guava (issue #1113), we should
+ avoid adding any new Guava imports here, especially in the API.
+
+-->
+
+<import-control pkg="io.opencensus">
+ <allow pkg="com.google.auto.value"/>
+ <allow pkg="com.google.errorprone.annotations"/>
+ <allow pkg="java"/>
+ <allow pkg="javax"/>
+ <allow class="io.grpc.Context"/>
+ <subpackage name="common">
+ <allow pkg="io.opencensus.common"/>
+ </subpackage>
+ <subpackage name="internal">
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.internal"/>
+ </subpackage>
+ <subpackage name="tags">
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.internal"/>
+ <allow pkg="io.opencensus.tags"/>
+ </subpackage>
+ <subpackage name="metrics">
+ <allow pkg="io.opencensus.internal"/>
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.metrics"/>
+ </subpackage>
+ <subpackage name="stats">
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.internal"/>
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ </subpackage>
+ <subpackage name="trace">
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.internal"/>
+ <allow pkg="io.opencensus.trace"/>
+
+ <!-- These dependencies on impl/implcore are only needed by -->
+ <!-- io.opencensus.trace.TraceComponentImpl and io.opencensus.trace.TraceComponentImplLite, -->
+ <!-- which are deprecated. -->
+ <allow class="io.opencensus.impl.internal.DisruptorEventQueue"/>
+ <allow class="io.opencensus.impl.trace.internal.ThreadLocalRandomHandler"/>
+ <allow class="io.opencensus.implcore.common.MillisClock"/>
+ <allow class="io.opencensus.implcore.internal.SimpleEventQueue"/>
+ <allow class="io.opencensus.implcore.trace.TraceComponentImplBase"/>
+ <allow class="io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler"/>
+ </subpackage>
+ <subpackage name="contrib">
+ <allow pkg="com.google.common"/>
+ <allow pkg="io.opencensus.common"/>
+ <subpackage name="agent">
+ <allow pkg="com.google.auto"/>
+ <allow pkg="com.typesafe.config"/>
+ <allow pkg="edu.umd.cs.findbugs.annotations"/>
+ <allow pkg="io.opencensus.contrib.agent"/>
+ <allow pkg="io.opencensus.trace"/>
+ <allow pkg="net.bytebuddy"/>
+ </subpackage>
+ <subpackage name="appengine.standard.util">
+ <allow pkg="com.google.apphosting"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="exemplar.util">
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="grpc.metrics">
+ <allow pkg="io.opencensus.contrib.grpc.metrics"/>
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ </subpackage>
+ <subpackage name="http.util">
+ <allow pkg="io.opencensus.contrib.http.util"/>
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="logcorrelation.log4j2">
+ <allow pkg="io.opencensus.contrib.logcorrelation.log4j2"/>
+ <allow pkg="io.opencensus.trace"/>
+ <disallow pkg="org.apache.logging.log4j.core.impl"/>
+ <allow pkg="org.apache.logging.log4j"/>
+ </subpackage>
+ <subpackage name="logcorrelation.stackdriver">
+ <allow pkg="com.google.cloud"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="spring">
+ <allow pkg="io.opencensus.trace"/>
+ <allow pkg="org.aspectj.lang"/>
+ <allow pkg="org.aspectj.lang.annotation"/>
+ <allow pkg="org.aspectj.lang.reflect"/>
+ <allow pkg="org.springframework.beans.factory.annotation"/>
+ <subpackage name="sleuth">
+ <allow pkg="io.opencensus.trace"/>
+ <allow pkg="org.apache.commons.logging"/>
+ <allow pkg="org.springframework.beans.factory.annotation"/>
+ <allow pkg="org.springframework.beans.factory.config"/>
+ <allow pkg="org.springframework.boot.autoconfigure"/>
+ <allow pkg="org.springframework.boot.context"/>
+ <allow pkg="org.springframework.context.annotation"/>
+ <allow pkg="org.springframework.boot.context.properties"/>
+ <allow pkg="org.springframework.cloud.sleuth"/>
+ <allow pkg="org.springframework.core"/>
+ </subpackage>
+ </subpackage>
+ <subpackage name="zpages">
+ <allow pkg="com.sun.net.httpserver"/>
+ <allow pkg="io.opencensus.contrib.grpc.metrics"/>
+ <allow pkg="io.opencensus.contrib.zpages"/>
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="monitoredresource.util">
+ <allow pkg="io.opencensus.contrib.monitoredresource.util"/>
+ </subpackage>
+ <subpackage name="dropwizard">
+ <allow pkg="io.opencensus.contrib.dropwizard"/>
+ <allow pkg="io.opencensus.metrics"/>
+ <allow pkg="io.opencensus.implcore"/>
+ <allow pkg="io.opencensus.internal"/>
+ <allow pkg="com.codahale.metrics"/>
+ </subpackage>
+ </subpackage>
+ <subpackage name="exporter">
+ <allow pkg="com.google.common"/>
+ <allow pkg="io.opencensus.common"/>
+ <subpackage name="stats">
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ <subpackage name="prometheus">
+ <allow pkg="io.opencensus.exporter.stats.prometheus"/>
+ <allow pkg="io.opencensus.trace"/>
+ <allow pkg="io.prometheus.client"/>
+ </subpackage>
+ <subpackage name="signalfx">
+ <allow pkg="com.signalfx"/>
+ <allow pkg="io.opencensus.exporter.stats.signalfx"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="stackdriver">
+ <allow pkg="com.google"/>
+ <allow pkg="io.opencensus.exporter.stats.stackdriver"/>
+ <allow pkg="io.opencensus.trace"/>
+ <allow pkg="io.opencensus.contrib.monitoredresource.util"/>
+ </subpackage>
+ </subpackage>
+ <subpackage name="trace">
+ <allow pkg="io.opencensus.trace"/>
+ <subpackage name="instana">
+ <allow pkg="io.opencensus.exporter.trace.instana"/>
+ </subpackage>
+ <subpackage name="jaeger">
+ <allow pkg="com.uber.jaeger"/>
+ <allow pkg="io.opencensus.exporter.trace.jaeger"/>
+ <allow pkg="org.apache.thrift"/>
+ </subpackage>
+ <subpackage name="ocagent">
+ <allow pkg="com.google.protobuf"/>
+ <allow pkg="io.grpc"/>
+ <allow pkg="io.opencensus.contrib.monitoredresource.util"/>
+ <allow pkg="io.opencensus.contrib.opencensus.proto.util"/>
+ <allow pkg="io.opencensus.exporter.trace.ocagent"/>
+ <allow pkg="io.opencensus.proto"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="stackdriver">
+ <allow pkg="com.google"/>
+ <allow pkg="io.opencensus.exporter.trace.stackdriver"/>
+ <allow pkg="io.opencensus.contrib.monitoredresource.util"/>
+ </subpackage>
+ <subpackage name="zipkin">
+ <allow pkg="io.opencensus.exporter.trace.zipkin"/>
+ <allow pkg="zipkin2"/>
+ </subpackage>
+ </subpackage>
+ </subpackage>
+ <subpackage name="implcore">
+ <allow pkg="com.google.common"/>
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.implcore"/>
+ <allow pkg="io.opencensus.metrics"/>
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="impl">
+ <allow pkg="com.lmax.disruptor"/>
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.impl"/>
+ <allow pkg="io.opencensus.implcore"/>
+ <allow pkg="io.opencensus.metrics"/>
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="impllite">
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.implcore"/>
+ <allow pkg="io.opencensus.impllite"/>
+ <allow pkg="io.opencensus.metrics"/>
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ <subpackage name="testing">
+ <allow pkg="com.google.common"/>
+ <allow pkg="io.opencensus.common"/>
+ <subpackage name="common">
+ <allow pkg="io.opencensus.testing.common"/>
+ </subpackage>
+ <subpackage name="export">
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ <allow pkg="io.opencensus.testing.export"/>
+ <allow pkg="io.opencensus.trace"/>
+ </subpackage>
+ </subpackage>
+ <subpackage name="examples">
+ <allow pkg="com.google.common"/>
+ <allow pkg="io.grpc"/>
+ <allow pkg="io.opencensus.common"/>
+ <allow pkg="io.opencensus.contrib"/>
+ <allow pkg="io.opencensus.examples"/>
+ <allow pkg="io.opencensus.exporter"/>
+ <allow pkg="io.opencensus.stats"/>
+ <allow pkg="io.opencensus.tags"/>
+ <allow pkg="io.opencensus.testing.export"/>
+ <allow pkg="io.opencensus.trace"/>
+ <allow pkg="io.prometheus"/>
+ </subpackage>
+</import-control>
diff --git a/buildscripts/kokoro/linux.cfg b/buildscripts/kokoro/linux.cfg
new file mode 100644
index 00000000..0d9e253b
--- /dev/null
+++ b/buildscripts/kokoro/linux.cfg
@@ -0,0 +1,5 @@
+# Config file for internal CI
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux.sh"
+timeout_mins: 60 \ No newline at end of file
diff --git a/buildscripts/kokoro/linux.sh b/buildscripts/kokoro/linux.sh
new file mode 100755
index 00000000..e8aa21be
--- /dev/null
+++ b/buildscripts/kokoro/linux.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# This file is used for Linux builds.
+# To run locally:
+# ./buildscripts/kokoro/linux.sh
+
+# This script assumes `set -e`. Removing it may lead to undefined behavior.
+set -exu -o pipefail
+
+# It would be nicer to use 'readlink -f' here but osx does not support it.
+readonly OPENCENSUS_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)"
+
+# cd to the root dir of opencensus-java
+cd $(dirname $0)/../..
+
+# Run tests
+./gradlew clean build
+
+OS=`uname`
+# Check the example only on Linux.
+if [ "$OS" = "Linux" ] ; then
+ pushd examples; ./gradlew clean assemble check --stacktrace; popd
+fi
diff --git a/buildscripts/kokoro/linux_build.cfg b/buildscripts/kokoro/linux_build.cfg
new file mode 100644
index 00000000..ddd15937
--- /dev/null
+++ b/buildscripts/kokoro/linux_build.cfg
@@ -0,0 +1,19 @@
+# Config file for child task BUILD
+
+env_vars {
+ key: "TASK"
+ value: "BUILD"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
+
+before_action {
+ fetch_keystore {
+ keystore_resource {
+ keystore_config_id: 73495
+ keyname: "codecov-auth-token"
+ }
+ }
+}
diff --git a/buildscripts/kokoro/linux_example_bazel.cfg b/buildscripts/kokoro/linux_example_bazel.cfg
new file mode 100644
index 00000000..3f4c872e
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_bazel.cfg
@@ -0,0 +1,10 @@
+# Config file for child task BUILD_EXAMPLES_BAZEL
+
+env_vars {
+ key: "TASK"
+ value: "BUILD_EXAMPLES_BAZEL"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_example_format.cfg b/buildscripts/kokoro/linux_example_format.cfg
new file mode 100644
index 00000000..6f9a3dc1
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_format.cfg
@@ -0,0 +1,9 @@
+# Config file for child task CHECK_EXAMPLES_FORMAT
+env_vars {
+ key: "TASK"
+ value: "CHECK_EXAMPLES_FORMAT"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_example_gradle.cfg b/buildscripts/kokoro/linux_example_gradle.cfg
new file mode 100644
index 00000000..7c14df77
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_gradle.cfg
@@ -0,0 +1,10 @@
+# Config file for child task BUILD_EXAMPLES_GRADLE
+
+env_vars {
+ key: "TASK"
+ value: "BUILD_EXAMPLES_GRADLE"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_example_license.cfg b/buildscripts/kokoro/linux_example_license.cfg
new file mode 100644
index 00000000..19cc67d0
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_license.cfg
@@ -0,0 +1,10 @@
+# Config file for child task CHECK_EXAMPLES_LICENSE
+
+env_vars {
+ key: "TASK"
+ value: "CHECK_EXAMPLES_LICENSE"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_example_maven.cfg b/buildscripts/kokoro/linux_example_maven.cfg
new file mode 100644
index 00000000..98f4a3b9
--- /dev/null
+++ b/buildscripts/kokoro/linux_example_maven.cfg
@@ -0,0 +1,10 @@
+# Config file for child task BUILD_EXAMPLES_MAVEN
+
+env_vars {
+ key: "TASK"
+ value: "BUILD_EXAMPLES_MAVEN"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_framework.cfg b/buildscripts/kokoro/linux_framework.cfg
new file mode 100644
index 00000000..112fc206
--- /dev/null
+++ b/buildscripts/kokoro/linux_framework.cfg
@@ -0,0 +1,10 @@
+# Config file for child task CHECKER_FRAMEWORK
+
+env_vars {
+ key: "TASK"
+ value: "CHECKER_FRAMEWORK"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_git_history.cfg b/buildscripts/kokoro/linux_git_history.cfg
new file mode 100644
index 00000000..5677835a
--- /dev/null
+++ b/buildscripts/kokoro/linux_git_history.cfg
@@ -0,0 +1,10 @@
+# Config file for child task CHECK_GIT_HISTORY
+
+env_vars {
+ key: "TASK"
+ value: "CHECK_GIT_HISTORY"
+}
+
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux_presubmit.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/linux_presubmit.sh b/buildscripts/kokoro/linux_presubmit.sh
new file mode 100755
index 00000000..bb1281b4
--- /dev/null
+++ b/buildscripts/kokoro/linux_presubmit.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+# This file is used for Linux builds.
+# It expects TASK environment variable is defined.
+# To run locally:
+# ./buildscripts/kokoro/linux.sh
+
+# This script assumes `set -e`. Removing it may lead to undefined behavior.
+set -exu -o pipefail
+
+# It would be nicer to use 'readlink -f' here but osx does not support it.
+readonly OPENCENSUS_JAVA_DIR="$(cd "$(dirname "$0")"/../.. && pwd)"
+
+# cd to the root dir of opencensus-java
+cd $(dirname $0)/../..
+
+valid_tasks() {
+ echo "Valid tasks are"
+ echo ""
+ echo "- BUILD"
+ echo "- BUILD_EXAMPLES_BAZEL"
+ echo "- BUILD_EXAMPLES_GRADLE"
+ echo "- BUILD_EXAMPLES_MAVEN"
+ echo "- CHECKER_FRAMEWORK"
+ echo "- CHECK_EXAMPLES_FORMAT"
+ echo "- CHECK_EXAMPLES_LICENSE"
+ echo "- CHECK_GIT_HISTORY"
+}
+
+if [[ ! -v TASK ]]; then
+ set +x
+ echo "TASK not set in environment"
+ valid_tasks
+ exit 1
+fi
+
+case "$TASK" in
+ "CHECK_GIT_HISTORY")
+ python ./scripts/check-git-history.py
+ ;;
+ "BUILD")
+ ./gradlew clean assemble --stacktrace
+ ./gradlew check :opencensus-all:jacocoTestReport
+ ./gradlew verGJF
+
+ # Run codecoverage reporting only if the script is running
+ # as a part of KOKORO BUILD. If it is outside of kokoro
+ # then there is no access to the codecov token and hence
+ # there is no point in running it.
+ if [[ -v KOKORO_BUILD_NUMBER ]]; then
+ # Get token from file located at
+ # $KOKORO_KEYSTORE_DIR/73495_codecov-auth-token
+ if [ -f $KOKORO_KEYSTORE_DIR/73495_codecov-auth-token ] ; then
+ curl -s https://codecov.io/bash | bash -s -- -Z -t @$KOKORO_KEYSTORE_DIR/73495_codecov-auth-token
+ else
+ echo "Codecov token file not found"
+ exit 1
+ fi
+ else
+ echo "Skipping codecov reporting"
+ fi
+ ;;
+ "CHECKER_FRAMEWORK")
+ ./gradlew clean assemble -PcheckerFramework=true
+ ;;
+ "CHECK_EXAMPLES_LICENSE")
+ curl -L -o checkstyle-8.12-all.jar https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.12/checkstyle-8.12-all.jar
+ java -DrootDir=. -jar checkstyle-8.12-all.jar -c buildscripts/checkstyle.xml examples/src/
+ ;;
+ "CHECK_EXAMPLES_FORMAT")
+ curl -L -o google-java-format-1.5-all-deps.jar \
+ https://github.com/google/google-java-format/releases/download/google-java-format-1.5/google-java-format-1.5-all-deps.jar
+ java -jar google-java-format-1.5-all-deps.jar --set-exit-if-changed --dry-run `find examples/src/ -name '*.java'`
+ ;;
+ "BUILD_EXAMPLES_GRADLE")
+ pushd examples && ./gradlew clean assemble --stacktrace && popd
+ ;;
+ "BUILD_EXAMPLES_MAVEN")
+ pushd examples && mvn clean package appassembler:assemble -e && popd
+ ;;
+ "BUILD_EXAMPLES_BAZEL")
+ pushd examples && bazel clean && bazel build :all && popd
+ ;;
+ *)
+ set +x
+ echo "Unknown task $TASK"
+ valid_tasks
+ exit 1
+ ;;
+esac
diff --git a/buildscripts/kokoro/macos.cfg b/buildscripts/kokoro/macos.cfg
new file mode 100644
index 00000000..fe3a9803
--- /dev/null
+++ b/buildscripts/kokoro/macos.cfg
@@ -0,0 +1,6 @@
+# Config file for internal CI
+
+# Same script is used for macos as it is for Linux.
+# Location of the continuous shell script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/linux.sh"
+timeout_mins: 60
diff --git a/buildscripts/kokoro/windows.bat b/buildscripts/kokoro/windows.bat
new file mode 100755
index 00000000..7787df07
--- /dev/null
+++ b/buildscripts/kokoro/windows.bat
@@ -0,0 +1,21 @@
+@rem ##########################################################################
+@rem
+@rem Script to set up Kokoro worker and run Windows tests
+@rem
+@rem ##########################################################################
+@rem
+@rem To run locally execute 'buildscript\kokoro\windows.bat'.
+type c:\VERSION
+
+@rem Enter repo root
+cd /d %~dp0\..\..
+
+@rem Clear JAVA_HOME to prevent a different Java version from being used
+set JAVA_HOME=
+set PATH=C:\Program Files\java\jdk1.8.0_152\bin;%PATH%
+
+cmd.exe /C "%cd%\gradlew.bat" clean build || exit /b 1
+pushd examples
+cmd.exe /C "%cd%\gradlew.bat" clean assemble check --stacktrace || exit /b 1
+popd
+
diff --git a/buildscripts/kokoro/windows.cfg b/buildscripts/kokoro/windows.cfg
new file mode 100644
index 00000000..e5ff9b08
--- /dev/null
+++ b/buildscripts/kokoro/windows.cfg
@@ -0,0 +1,5 @@
+# Config file for internal CI
+
+# Location of the continuous windows batch script in repository.
+build_file: "opencensus-java/buildscripts/kokoro/windows.bat"
+timeout_mins: 60
diff --git a/checker-framework/stubs/grpc.astub b/checker-framework/stubs/grpc.astub
new file mode 100644
index 00000000..f8581eba
--- /dev/null
+++ b/checker-framework/stubs/grpc.astub
@@ -0,0 +1,12 @@
+package io.grpc;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+class Context {
+ static <T> Key<@Nullable T> key(String name);
+ static <T> Key<T> keyWithDefault(String name, T defaultValue);
+ class Key<T> {
+ T get(Context context);
+ T get();
+ }
+}
diff --git a/checker-framework/stubs/guava.astub b/checker-framework/stubs/guava.astub
new file mode 100644
index 00000000..42ed251e
--- /dev/null
+++ b/checker-framework/stubs/guava.astub
@@ -0,0 +1,14 @@
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+package com.google.common.base;
+
+class Strings {
+ @EnsuresNonNullIf(result = false, expression = "#1")
+ static boolean isNullOrEmpty(@Nullable String str);
+}
+
+class Preconditions {
+ static <T extends @NonNull Object> T checkNotNull(T reference, @Nullable Object errorMessage);
+}
diff --git a/checker-framework/stubs/log4j.astub b/checker-framework/stubs/log4j.astub
new file mode 100644
index 00000000..20b3240e
--- /dev/null
+++ b/checker-framework/stubs/log4j.astub
@@ -0,0 +1,8 @@
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+package org.apache.logging.log4j;
+
+class ThreadContext {
+ @Nullable
+ static ReadOnlyThreadContextMap getThreadContextMap();
+}
diff --git a/checker-framework/stubs/org-springframework-cloud-sleuth.astub b/checker-framework/stubs/org-springframework-cloud-sleuth.astub
new file mode 100644
index 00000000..61d2fa11
--- /dev/null
+++ b/checker-framework/stubs/org-springframework-cloud-sleuth.astub
@@ -0,0 +1,19 @@
+package org.springframework.cloud.sleuth;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.cloud.sleuth.Sampler;
+import org.springframework.cloud.sleuth.Span;
+
+interface Tracer {
+ @Nullable Span close(@Nullable Span span);
+ @Nullable Span continueSpan(@Nullable Span span);
+ @Nullable Span createSpan(String name);
+ @Nullable Span createSpan(String name, @Nullable Sampler sampler);
+ @Nullable Span createSpan(String name, @Nullable Span parent);
+ @Nullable Span detach(@Nullable Span span);
+ @Nullable Span getCurrentSpan();
+}
+
+class Span {
+ Span (Span span, @Nullable Span parent);
+}
diff --git a/checker-framework/stubs/org-springframework-cloud-sleuth.log.astub b/checker-framework/stubs/org-springframework-cloud-sleuth.log.astub
new file mode 100644
index 00000000..9497f6f2
--- /dev/null
+++ b/checker-framework/stubs/org-springframework-cloud-sleuth.log.astub
@@ -0,0 +1,9 @@
+package org.springframework.cloud.sleuth.log;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.springframework.cloud.sleuth.Span;
+
+interface SpanLogger {
+ void logStartedSpan(@Nullable Span parent, Span span);
+ void logStoppedSpan(@Nullable Span parent, Span span);
+}
diff --git a/contrib/agent/README.md b/contrib/agent/README.md
new file mode 100644
index 00000000..f24c28a2
--- /dev/null
+++ b/contrib/agent/README.md
@@ -0,0 +1,95 @@
+# OpenCensus Agent for Java
+
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Agent for Java* collects and sends latency data about your Java process to
+OpenCensus backends such as Zipkin, Stackdriver Trace, etc. for analysis and visualization.
+
+
+## Features
+
+The *OpenCensus Agent for Java* is in an early development stage. The following features are
+currently implemented:
+
+TODO(stschmidt): Update README.md along with implementation.
+
+
+### Automatic context propagation for Executors
+
+The context of the caller of [Executor#execute](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html#execute-java.lang.Runnable-)
+is automatically propagated to the submitted Runnable.
+
+
+### Automatic context propagation for Threads
+
+The context of the caller of [Thread#start](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#start--)
+is automatically propagated to the new thread.
+
+
+### Preliminary support for tracing
+
+As a proof-of-concept, the agent wraps the execution of
+[URL#getContent](https://docs.oracle.com/javase/8/docs/api/java/net/URL.html#getContent--) in a new
+trace span.
+
+
+## Design Ideas
+
+We see tracing as a cross-cutting concern which the *OpenCensus Agent for Java* weaves into
+existing Java bytecode (the application and its libraries) at runtime, typically when first loading
+the concerned bytecode.
+
+This approach allows us to instrument arbitrary code without having to touch the source code of the
+application or its dependencies. Furthermore, we don't require the application owner to upgrade any
+of the application's third-party dependencies to specific versions. As long as the interface (e.g.
+[java.sql.Driver#connect](https://docs.oracle.com/javase/8/docs/api/java/sql/Driver.html#connect-java.lang.String-java.util.Properties-))
+stays as-is across the supported versions, the Java agent's bytecode weaver will be able to
+instrument the code.
+
+The *OpenCensus Agent for Java* uses [Byte Buddy](http://bytebuddy.net/), a widely used and
+well-maintained bytecode manipulation library, for instrumenting selected Java methods at class
+load-time. Which Java methods we want to intercept/instrument obviously depends on the library
+(MongoDB vs. Redis, etc.) and the application.
+
+
+## Installation and Usage
+
+Download the latest version of the *OpenCensus Agent for Java* `.jar` file
+from [Maven Central][maven-url]. Store it somewhere on disk.
+
+To enable the *OpenCensus Agent for Java* for your application, add the option
+`-javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar` to the invocation of the `java`
+executable as shown in the following example. Replace `X.Y.Z` with the actual version number.
+
+```shell
+java -javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar ...
+```
+
+
+## Configuration
+
+The *OpenCensus Agent for Java* uses [Typesafe's configuration
+library](https://lightbend.github.io/config/) for all user-configurable settings. Please refer to
+[reference.conf](src/main/resources/reference.conf) for the available configuration knobs and their
+defaults.
+
+You can override the default configuration in [different
+ways](https://github.com/lightbend/config/blob/7cae92d3ae3ff9d06f1db43800232d2f73c6fe44/README.md#standard-behavior).
+For example, to disable the automatic context propagation for Executors, add a system property as
+follows:
+
+```shell
+java -javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar \
+ -Dopencensus.contrib.agent.context-propagation.executor.enabled=false \
+ ...
+```
+
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-agent/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-agent
diff --git a/contrib/agent/build.gradle b/contrib/agent/build.gradle
new file mode 100644
index 00000000..11271a42
--- /dev/null
+++ b/contrib/agent/build.gradle
@@ -0,0 +1,246 @@
+plugins {
+ id 'com.github.johnrengelman.shadow' version '2.0.2'
+}
+
+description = 'OpenCensus Agent'
+
+def agentPackage = 'io.opencensus.contrib.agent'
+def agentMainClass = "${agentPackage}.AgentMain"
+
+// The package containing the classes that need to be loaded by the bootstrap classloader because
+// they are used from classes loaded by the bootstrap classloader.
+def agentBootstrapPackage = "${agentPackage}.bootstrap"
+def agentBootstrapPackageDir = agentBootstrapPackage.replace('.', '/') + '/'
+def agentBootstrapClasses = agentBootstrapPackageDir + '**'
+
+// The package to which we relocate all third party packages. This avoids any conflicts of the
+// agent's classes with the app's classes, which are loaded by the same classloader (the system
+// classloader).
+def agentRepackaged = "${agentPackage}.deps"
+
+dependencies {
+ compileOnly libraries.auto_service
+ compileOnly libraries.grpc_context
+ compileOnly project(':opencensus-api')
+ compile libraries.byte_buddy
+ compile libraries.config
+ compile libraries.findbugs_annotations
+ compile libraries.guava
+
+ signature 'org.codehaus.mojo.signature:java17:1.0@signature'
+}
+
+jar {
+ manifest {
+ // Set the required manifest attributes for the Java agent, cf.
+ // https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html.
+ attributes 'Premain-Class': agentMainClass
+ attributes 'Can-Retransform-Classes': true
+ }
+}
+
+// Create bootstrap.jar containing the classes that need to be loaded by the bootstrap
+// classloader.
+task bootstrapJar(type: Jar) {
+ // Output to 'bootstrap.jar'.
+ baseName = 'bootstrap'
+ version = null
+
+ from sourceSets.main.output
+ include agentBootstrapClasses
+}
+
+shadowJar.dependsOn bootstrapJar
+
+// Bundle the agent's classes and dependencies into a single, self-contained JAR file.
+shadowJar {
+ // Output to opencensus-contrib-agent-VERSION.jar.
+ classifier = null
+
+ // Include only the following dependencies (excluding transitive dependencies).
+ dependencies {
+ include(dependency(libraries.byte_buddy))
+ include(dependency(libraries.config))
+ include(dependency(libraries.guava))
+ }
+
+ // Exclude cruft which still snuck in.
+ exclude 'META-INF/maven/**'
+ exclude agentBootstrapClasses
+
+ // Relocate third party packages to avoid any conflicts of the agent's classes with the app's
+ // classes, which are loaded by the same classloader (the system classloader).
+ // Byte Buddy:
+ relocate 'net.bytebuddy', agentRepackaged + '.bytebuddy'
+ // Config:
+ relocate 'com.typesafe.config', agentRepackaged + '.config'
+ // Guava:
+ relocate 'com.google.common', agentRepackaged + '.guava'
+ relocate 'com.google.thirdparty.publicsuffix', agentRepackaged + '.publicsuffix'
+
+ doLast {
+ def agentPackageDir = agentPackage.replace('.', '/') + '/'
+ def agentBootstrapJar = agentPackageDir + 'bootstrap.jar'
+
+ // Bundle bootstrap.jar.
+ ant.jar(update: 'true', destfile: shadowJar.archivePath) {
+ mappedresources {
+ fileset(file: bootstrapJar.archivePath)
+ globmapper(from: '*', to: agentBootstrapJar)
+ }
+ }
+
+ // Assert that there's nothing obviously wrong with the JAR's contents.
+ new java.util.zip.ZipFile(shadowJar.archivePath).withCloseable {
+ // Must have bundled the bootstrap.jar.
+ assert it.entries().any { it.name == agentBootstrapJar }
+
+ it.entries().each { entry ->
+ // Must not contain anything outside of ${agentPackage}, ...
+ assert entry.name.startsWith(agentPackageDir) ||
+ // ... except for the expected entries.
+ [ agentPackageDir,
+ 'META-INF/MANIFEST.MF',
+ 'META-INF/services/io.opencensus.contrib.agent.instrumentation.Instrumenter',
+ 'reference.conf',
+ ].any { entry.isDirectory() ? it.startsWith(entry.name) : it == entry.name }
+ // Also, should not have the bootstrap classes.
+ assert !entry.name.startsWith(agentBootstrapPackageDir)
+ }
+ }
+ }
+}
+
+jar.finalizedBy shadowJar
+
+// TODO(stschmidt): Proguard-shrink the agent JAR.
+
+// Integration tests. The setup was initially based on
+// https://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing/.
+// We run the same suite of integration tests on different Java versions with the agent enabled.
+// The JAVA_HOMES environment variable lists the home directories of the Java installations used
+// for integration testing.
+
+// The default JAR has been replaced with a self-contained JAR by the shadowJar task. Therefore,
+// remove all declared dependencies from the generated Maven POM for said JAR.
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ pom.whenConfigured {
+ dependencies = []
+ }
+ }
+ }
+}
+
+sourceSets {
+ integrationTest {
+ java {
+ compileClasspath += main.output + test.output
+ runtimeClasspath += main.output + test.output
+ srcDir file('src/integration-test/java')
+ }
+ resources.srcDir file('src/integration-test/resources')
+ }
+}
+
+configurations {
+ integrationTestCompile.extendsFrom testCompile
+ integrationTestRuntime.extendsFrom testRuntime
+}
+
+dependencies {
+ integrationTestCompile project(':opencensus-api')
+ integrationTestCompile project(':opencensus-testing')
+ integrationTestRuntime libraries.grpc_context
+ integrationTestRuntime project(':opencensus-impl-lite')
+}
+
+// Disable checkstyle for integration tests if not java8.
+checkstyleIntegrationTest.enabled = JavaVersion.current().isJava8Compatible()
+
+// Disable findbugs for integration tests, too.
+findbugsIntegrationTest.enabled = false
+
+def javaExecutables = (System.getenv('JAVA_HOMES') ?: '')
+ .tokenize(File.pathSeparator)
+ .plus(System.getProperty('java.home'))
+ .collect { org.apache.tools.ant.taskdefs.condition.Os.isFamily(
+ org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS)
+ ? "${it}/bin/java.exe"
+ : "${it}/bin/java" }
+ .collect { new File(it).getCanonicalPath() }
+ .unique()
+
+assert javaExecutables.size > 0 :
+ 'No Java executables found for running integration tests'
+
+task integrationTest
+
+javaExecutables.eachWithIndex { javaExecutable, index ->
+ def perVersionIntegrationTest = task("integrationTest_${index}", type: Test) {
+ testLogging {
+ // Let Gradle output the stdout and stderr from tests, too. This is useful for investigating
+ // test failures on Travis, where we can't view Gradle's test reports.
+ showStandardStreams = true
+
+ // Include the exception message and full stacktrace for failed tests.
+ exceptionFormat 'full'
+ }
+
+ dependsOn shadowJar
+
+ testClassesDirs = sourceSets.integrationTest.output.classesDirs
+ classpath = sourceSets.integrationTest.runtimeClasspath
+
+ executable = javaExecutable
+
+ // The JaCoCo agent must be specified first so that it can instrument our agent.
+ // This is a work around for the issue that the JaCoCo agent is added last, cf.
+ // https://discuss.gradle.org/t/jacoco-gradle-adds-the-agent-last-to-jvm-args/7124.
+ doFirst {
+ jvmArgs jacoco.asJvmArg // JaCoCo agent first.
+ jvmArgs "-javaagent:${shadowJar.archivePath}" // Our agent second.
+ jacoco.enabled = false // Don't add the JaCoCo agent again.
+ }
+
+ doFirst { logger.lifecycle("Running integration tests using ${javaExecutable}.") }
+ }
+
+ integrationTest.dependsOn perVersionIntegrationTest
+}
+
+check.dependsOn integrationTest
+integrationTest.mustRunAfter test
+
+// Merge JaCoCo's execution data from all tests into the main test's execution data file.
+task jacocoMerge(type: JacocoMerge) {
+ tasks.withType(Test).each { testTask ->
+ dependsOn testTask
+ executionData testTask.jacoco.destinationFile
+ }
+ doLast {
+ destinationFile.renameTo test.jacoco.destinationFile
+ }
+}
+
+jacocoTestReport.dependsOn jacocoMerge
+
+// JMH benchmarks
+
+dependencies {
+ jmh libraries.grpc_context
+}
+
+// Make the agent JAR available using a fixed file name so that we don't have to modify the JMH
+// benchmarks whenever the version changes.
+task agentJar(type: Copy) {
+ dependsOn shadowJar
+
+ from shadowJar.archivePath
+ into libsDir
+ rename { 'agent.jar' }
+}
+
+jmhJar.dependsOn agentJar
+jmhJar.dependsOn integrationTest
diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java
new file mode 100644
index 00000000..7cab5590
--- /dev/null
+++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java
@@ -0,0 +1,195 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.grpc.Context;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Integration tests for {@link ExecutorInstrumentation}.
+ *
+ * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via
+ * the {@code -javaagent} command line option.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
+public class ExecutorInstrumentationIT {
+
+ private static final Context.Key<String> KEY = Context.key("mykey");
+
+ private ExecutorService executor;
+ private Context previousContext;
+
+ @Before
+ public void beforeMethod() {
+ executor = Executors.newCachedThreadPool();
+ }
+
+ @After
+ public void afterMethod() {
+ Context.current().detach(previousContext);
+ executor.shutdown();
+ }
+
+ @Test(timeout = 60000)
+ public void execute() throws Exception {
+ final Thread callerThread = Thread.currentThread();
+ final Context context = Context.current().withValue(KEY, "myvalue");
+ previousContext = context.attach();
+
+ final Semaphore tested = new Semaphore(0);
+
+ executor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ assertThat(Thread.currentThread()).isNotSameAs(callerThread);
+ assertThat(Context.current()).isSameAs(context);
+ assertThat(KEY.get()).isEqualTo("myvalue");
+ tested.release();
+ }
+ });
+
+ tested.acquire();
+ }
+
+ @Test(timeout = 60000)
+ public void submit_Callable() throws Exception {
+ final Thread callerThread = Thread.currentThread();
+ final Context context = Context.current().withValue(KEY, "myvalue");
+ previousContext = context.attach();
+
+ final AtomicBoolean tested = new AtomicBoolean(false);
+
+ executor
+ .submit(
+ new Callable<Void>() {
+ @Override
+ public Void call() throws Exception {
+ assertThat(Thread.currentThread()).isNotSameAs(callerThread);
+ assertThat(Context.current()).isSameAs(context);
+ assertThat(KEY.get()).isEqualTo("myvalue");
+ tested.set(true);
+
+ return null;
+ }
+ })
+ .get();
+
+ assertThat(tested.get()).isTrue();
+ }
+
+ @Test(timeout = 60000)
+ public void submit_Runnable() throws Exception {
+ final Thread callerThread = Thread.currentThread();
+ final Context context = Context.current().withValue(KEY, "myvalue");
+ previousContext = context.attach();
+
+ final AtomicBoolean tested = new AtomicBoolean(false);
+
+ executor
+ .submit(
+ new Runnable() {
+ @Override
+ public void run() {
+ assertThat(Thread.currentThread()).isNotSameAs(callerThread);
+ assertThat(Context.current()).isSameAs(context);
+ assertThat(KEY.get()).isEqualTo("myvalue");
+ tested.set(true);
+ }
+ })
+ .get();
+
+ assertThat(tested.get()).isTrue();
+ }
+
+ @Test(timeout = 60000)
+ public void submit_RunnableWithResult() throws Exception {
+ final Thread callerThread = Thread.currentThread();
+ final Context context = Context.current().withValue(KEY, "myvalue");
+ previousContext = context.attach();
+
+ final AtomicBoolean tested = new AtomicBoolean(false);
+ Object result = new Object();
+
+ Future<Object> future =
+ executor.submit(
+ new Runnable() {
+ @Override
+ public void run() {
+ assertThat(Thread.currentThread()).isNotSameAs(callerThread);
+ assertThat(Context.current()).isNotSameAs(Context.ROOT);
+ assertThat(Context.current()).isSameAs(context);
+ assertThat(KEY.get()).isEqualTo("myvalue");
+ tested.set(true);
+ }
+ },
+ result);
+
+ assertThat(future.get()).isSameAs(result);
+ assertThat(tested.get()).isTrue();
+ }
+
+ @Test(timeout = 60000)
+ public void currentContextExecutor() throws Exception {
+ final Thread callerThread = Thread.currentThread();
+ final Context context = Context.current().withValue(KEY, "myvalue");
+ previousContext = context.attach();
+
+ final Semaphore tested = new Semaphore(0);
+
+ Context.currentContextExecutor(executor)
+ .execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ StackTraceElement[] ste = new Exception().fillInStackTrace().getStackTrace();
+ assertThat(ste[0].getClassName()).doesNotContain("Context");
+ assertThat(ste[1].getClassName()).startsWith("io.grpc.Context$");
+ // NB: Actually, we want the Runnable to be wrapped only once, but currently it is
+ // still wrapped twice. The two places where the Runnable is wrapped are: (1) the
+ // executor implementation itself, e.g. ThreadPoolExecutor, to which the Agent added
+ // automatic context propagation, (2) CurrentContextExecutor.
+ // ExecutorInstrumentation already avoids adding the automatic context propagation
+ // to CurrentContextExecutor, but does not make it a no-op yet. Also see
+ // ExecutorInstrumentation#createMatcher.
+ assertThat(ste[2].getClassName()).startsWith("io.grpc.Context$");
+ assertThat(ste[3].getClassName()).doesNotContain("Context");
+
+ assertThat(Thread.currentThread()).isNotSameAs(callerThread);
+ assertThat(Context.current()).isSameAs(context);
+ assertThat(KEY.get()).isEqualTo("myvalue");
+
+ tested.release();
+ }
+ });
+
+ tested.acquire();
+ }
+}
diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java
new file mode 100644
index 00000000..f718f492
--- /dev/null
+++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java
@@ -0,0 +1,144 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.grpc.Context;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Integration tests for {@link ThreadInstrumentation}.
+ *
+ * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via
+ * the {@code -javaagent} command line option.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
+public class ThreadInstrumentationIT {
+
+ private static final Context.Key<String> KEY = Context.key("mykey");
+
+ private Context previousContext;
+
+ @After
+ public void afterMethod() {
+ Context.current().detach(previousContext);
+ }
+
+ @Test(timeout = 60000)
+ public void start_Runnable() throws Exception {
+ final Context context = Context.current().withValue(KEY, "myvalue");
+ previousContext = context.attach();
+
+ final AtomicBoolean tested = new AtomicBoolean(false);
+
+ Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ assertThat(Context.current()).isSameAs(context);
+ assertThat(KEY.get()).isEqualTo("myvalue");
+ tested.set(true);
+ }
+ };
+ Thread thread = new Thread(runnable);
+
+ thread.start();
+ thread.join();
+
+ assertThat(tested.get()).isTrue();
+ }
+
+ @Test(timeout = 60000)
+ public void start_Subclass() throws Exception {
+ final Context context = Context.current().withValue(KEY, "myvalue");
+ previousContext = context.attach();
+
+ final AtomicBoolean tested = new AtomicBoolean(false);
+
+ class MyThread extends Thread {
+
+ @Override
+ public void run() {
+ assertThat(Context.current()).isSameAs(context);
+ assertThat(KEY.get()).isEqualTo("myvalue");
+ tested.set(true);
+ }
+ }
+
+ Thread thread = new MyThread();
+
+ thread.start();
+ thread.join();
+
+ assertThat(tested.get()).isTrue();
+ }
+
+ /**
+ * Tests that the automatic context propagation added by {@link ThreadInstrumentation} does not
+ * interfere with the automatically propagated context from Executor#execute.
+ */
+ @Test(timeout = 60000)
+ public void start_automaticallyWrappedRunnable() throws Exception {
+ final Context context = Context.current().withValue(KEY, "myvalue");
+ previousContext = context.attach();
+
+ Executor newThreadExecutor =
+ new Executor() {
+ @Override
+ public void execute(Runnable command) {
+ // Attach a new context before starting a new thread. This new context will be
+ // propagated to the new thread as in #start_Runnable. However, since the Runnable has
+ // been wrapped in a different context (by automatic instrumentation of
+ // Executor#execute), that context will be attached when executing the Runnable.
+ Context context2 = Context.current().withValue(KEY, "wrong context");
+ Context context3 = context2.attach();
+ try {
+ Thread thread = new Thread(command);
+ thread.start();
+ try {
+ thread.join();
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ } finally {
+ context2.detach(context3);
+ }
+ }
+ };
+
+ final AtomicReference<Context> newThreadCtx = new AtomicReference<Context>();
+ newThreadExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ newThreadCtx.set(Context.current());
+ }
+ });
+
+ // Assert that the automatic context propagation added by ThreadInstrumentation did not
+ // interfere with the automatically propagated context from Executor#execute.
+ assertThat(newThreadCtx.get()).isSameAs(context);
+ }
+}
diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java
new file mode 100644
index 00000000..163f3cd8
--- /dev/null
+++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java
@@ -0,0 +1,87 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+import io.opencensus.testing.export.TestHandler;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Integration tests for {@link UrlInstrumentation}.
+ *
+ * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via
+ * the {@code -javaagent} command line option.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
+public class UrlInstrumentationIT {
+
+ private static final TestHandler testHandler = new TestHandler();
+
+ @BeforeClass
+ public static void beforeClass() {
+ Tracing.getExportComponent().getSpanExporter().registerHandler("test", testHandler);
+ }
+
+ @AfterClass
+ public static void afterClass() {
+ Tracing.getExportComponent().getSpanExporter().unregisterHandler("test");
+ }
+
+ @Test(timeout = 60000)
+ public void getContent() throws Exception {
+ URL url = getClass().getResource("some_resource.txt").toURI().toURL();
+ Object content = url.getContent();
+
+ assertThat(content).isInstanceOf(InputStream.class);
+ assertThat(CharStreams.toString(new InputStreamReader((InputStream) content, Charsets.UTF_8)))
+ .isEqualTo("Some resource.");
+
+ SpanData span = testHandler.waitForExport(1).get(0);
+ assertThat(span.getName()).isEqualTo("java.net.URL#getContent");
+ assertThat(span.getStatus().isOk()).isTrue();
+ }
+
+ @Test(timeout = 60000)
+ public void getContent_fails() throws MalformedURLException {
+ URL url = new URL("file:///nonexistent");
+
+ try {
+ url.getContent();
+ fail();
+ } catch (IOException e) {
+ SpanData span = testHandler.waitForExport(1).get(0);
+ assertThat(span.getName()).isEqualTo("java.net.URL#getContent");
+ assertThat(span.getStatus().isOk()).isFalse();
+ }
+ }
+}
diff --git a/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt b/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt
new file mode 100644
index 00000000..7e8787cb
--- /dev/null
+++ b/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt
@@ -0,0 +1 @@
+Some resource. \ No newline at end of file
diff --git a/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java
new file mode 100644
index 00000000..7c2d4423
--- /dev/null
+++ b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.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.contrib.agent.instrumentation;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import io.grpc.Context;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.infra.Blackhole;
+
+/** Benchmarks for automatic context propagation added by {@link ExecutorInstrumentation}. */
+public class ExecutorInstrumentationBenchmark {
+
+ private static final class MyRunnable implements Runnable {
+
+ private final Blackhole blackhole;
+
+ private MyRunnable(Blackhole blackhole) {
+ this.blackhole = blackhole;
+ }
+
+ @Override
+ public void run() {
+ blackhole.consume(Context.current());
+ }
+ }
+
+ /**
+ * This benchmark attempts to measure the performance without any context propagation.
+ *
+ * @param blackhole a {@link Blackhole} object supplied by JMH
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ @Fork
+ public void none(final Blackhole blackhole) {
+ MoreExecutors.directExecutor().execute(new MyRunnable(blackhole));
+ }
+
+ /**
+ * This benchmark attempts to measure the performance with manual context propagation.
+ *
+ * @param blackhole a {@link Blackhole} object supplied by JMH
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ @Fork
+ public void manual(final Blackhole blackhole) {
+ MoreExecutors.directExecutor().execute(Context.current().wrap(new MyRunnable(blackhole)));
+ }
+
+ /**
+ * This benchmark attempts to measure the performance with automatic context propagation.
+ *
+ * @param blackhole a {@link Blackhole} object supplied by JMH
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ @Fork(jvmArgsAppend = "-javaagent:contrib/agent/build/libs/agent.jar")
+ public void automatic(final Blackhole blackhole) {
+ MoreExecutors.directExecutor().execute(new MyRunnable(blackhole));
+ }
+}
diff --git a/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java
new file mode 100644
index 00000000..706c6d3a
--- /dev/null
+++ b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java
@@ -0,0 +1,89 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import io.grpc.Context;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.infra.Blackhole;
+
+/** Naive benchmarks for automatic context propagation added by {@link ThreadInstrumentation}. */
+public class ThreadInstrumentationBenchmark {
+
+ private static final class MyRunnable implements Runnable {
+
+ private final Blackhole blackhole;
+
+ private MyRunnable(Blackhole blackhole) {
+ this.blackhole = blackhole;
+ }
+
+ @Override
+ public void run() {
+ blackhole.consume(Context.current());
+ }
+ }
+
+ /**
+ * This benchmark attempts to measure the performance without any context propagation.
+ *
+ * @param blackhole a {@link Blackhole} object supplied by JMH
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.MICROSECONDS)
+ @Fork
+ public void none(Blackhole blackhole) throws InterruptedException {
+ Thread t = new Thread(new MyRunnable(blackhole));
+ t.start();
+ t.join();
+ }
+
+ /**
+ * This benchmark attempts to measure the performance with manual context propagation.
+ *
+ * @param blackhole a {@link Blackhole} object supplied by JMH
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.MICROSECONDS)
+ @Fork
+ public void manual(Blackhole blackhole) throws InterruptedException {
+ Thread t = new Thread((Context.current().wrap(new MyRunnable(blackhole))));
+ t.start();
+ t.join();
+ }
+
+ /**
+ * This benchmark attempts to measure the performance with automatic context propagation.
+ *
+ * @param blackhole a {@link Blackhole} object supplied by JMH
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ @OutputTimeUnit(TimeUnit.MICROSECONDS)
+ @Fork(jvmArgsAppend = "-javaagent:contrib/agent/build/libs/agent.jar")
+ public void automatic(Blackhole blackhole) throws InterruptedException {
+ Thread t = new Thread(new MyRunnable(blackhole));
+ t.start();
+ t.join();
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java
new file mode 100644
index 00000000..54a82442
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java
@@ -0,0 +1,68 @@
+/*
+ * 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.contrib.agent;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType;
+import net.bytebuddy.utility.JavaModule;
+
+/**
+ * An {@link AgentBuilder.Listener} which uses {@link java.util.logging} for logging events of
+ * interest.
+ */
+final class AgentBuilderListener implements AgentBuilder.Listener {
+
+ private static final Logger logger = Logger.getLogger(AgentBuilderListener.class.getName());
+
+ @Override
+ public void onTransformation(
+ TypeDescription typeDescription,
+ ClassLoader classLoader,
+ JavaModule module,
+ boolean loaded,
+ DynamicType dynamicType) {
+ logger.log(Level.FINE, "{0}", typeDescription);
+ }
+
+ @Override
+ public void onIgnored(
+ TypeDescription typeDescription,
+ ClassLoader classLoader,
+ JavaModule module,
+ boolean loaded) {}
+
+ @Override
+ public void onError(
+ String typeName,
+ ClassLoader classLoader,
+ JavaModule module,
+ boolean loaded,
+ Throwable throwable) {
+ logger.log(Level.WARNING, "Failed to handle " + typeName, throwable);
+ }
+
+ @Override
+ public void onComplete(
+ String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {}
+
+ @Override
+ public void onDiscovery(
+ String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {}
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java
new file mode 100644
index 00000000..49c568ed
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java
@@ -0,0 +1,97 @@
+/*
+ * 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.contrib.agent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static net.bytebuddy.matcher.ElementMatchers.none;
+
+import io.opencensus.contrib.agent.bootstrap.ContextStrategy;
+import io.opencensus.contrib.agent.bootstrap.ContextTrampoline;
+import io.opencensus.contrib.agent.instrumentation.Instrumenter;
+import java.lang.instrument.Instrumentation;
+import java.util.ServiceLoader;
+import java.util.jar.JarFile;
+import java.util.logging.Logger;
+import net.bytebuddy.agent.builder.AgentBuilder;
+
+/**
+ * The <b>OpenCensus Agent for Java</b> collects and sends latency data about your Java process to
+ * OpenCensus backends such as Stackdriver Trace for analysis and visualization.
+ *
+ * <p>To enable the *OpenCensus Agent for Java* for your application, add the option {@code
+ * -javaagent:path/to/opencensus-contrib-agent.jar} to the invocation of the {@code java} executable
+ * as shown in the following example:
+ *
+ * <pre>
+ * java -javaagent:path/to/opencensus-contrib-agent.jar ...
+ * </pre>
+ *
+ * @see <a
+ * href="https://github.com/census-instrumentation/instrumentation-java/tree/master/agent">https://github.com/census-instrumentation/instrumentation-java/tree/master/agent</a>
+ * @since 0.6
+ */
+public final class AgentMain {
+
+ private static final Logger logger = Logger.getLogger(AgentMain.class.getName());
+
+ private AgentMain() {}
+
+ /**
+ * Initializes the OpenCensus Agent for Java.
+ *
+ * @param agentArgs agent options, passed as a single string by the JVM
+ * @param instrumentation the {@link Instrumentation} object provided by the JVM for instrumenting
+ * Java programming language code
+ * @throws Exception if initialization of the agent fails
+ * @see java.lang.instrument
+ * @since 0.6
+ */
+ public static void premain(String agentArgs, Instrumentation instrumentation) throws Exception {
+ checkNotNull(instrumentation, "instrumentation");
+
+ logger.fine("Initializing.");
+
+ // The classes in bootstrap.jar, such as ContextManger and ContextStrategy, will be referenced
+ // from classes loaded by the bootstrap classloader. Thus, these classes have to be loaded by
+ // the bootstrap classloader, too.
+ instrumentation.appendToBootstrapClassLoaderSearch(
+ new JarFile(Resources.getResourceAsTempFile("bootstrap.jar")));
+
+ checkLoadedByBootstrapClassloader(ContextTrampoline.class);
+ checkLoadedByBootstrapClassloader(ContextStrategy.class);
+
+ Settings settings = Settings.load();
+ AgentBuilder agentBuilder =
+ new AgentBuilder.Default()
+ .disableClassFormatChanges()
+ .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
+ .with(new AgentBuilderListener())
+ .ignore(none());
+ for (Instrumenter instrumenter : ServiceLoader.load(Instrumenter.class)) {
+ agentBuilder = instrumenter.instrument(agentBuilder, settings);
+ }
+ agentBuilder.installOn(instrumentation);
+
+ logger.fine("Initialized.");
+ }
+
+ private static void checkLoadedByBootstrapClassloader(Class<?> clazz) {
+ checkState(
+ clazz.getClassLoader() == null, "%s must be loaded by the bootstrap classloader", clazz);
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java
new file mode 100644
index 00000000..7367b85a
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java
@@ -0,0 +1,77 @@
+/*
+ * 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.contrib.agent;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Helper methods for working with resources. */
+final class Resources {
+ private Resources() {}
+
+ /**
+ * Returns a resource of the given name as a temporary file.
+ *
+ * @param resourceName name of the resource
+ * @return a temporary {@link File} containing a copy of the resource
+ * @throws FileNotFoundException if no resource of the given name is found
+ * @throws IOException if an I/O error occurs
+ */
+ static File getResourceAsTempFile(String resourceName) throws IOException {
+ checkArgument(!Strings.isNullOrEmpty(resourceName), "resourceName");
+
+ File file = File.createTempFile(resourceName, ".tmp");
+ OutputStream os = new FileOutputStream(file);
+ try {
+ getResourceAsTempFile(resourceName, file, os);
+ return file;
+ } finally {
+ os.close();
+ }
+ }
+
+ @VisibleForTesting
+ static void getResourceAsTempFile(String resourceName, File file, OutputStream outputStream)
+ throws IOException {
+ file.deleteOnExit();
+
+ InputStream is = getResourceAsStream(resourceName);
+ try {
+ ByteStreams.copy(is, outputStream);
+ } finally {
+ is.close();
+ }
+ }
+
+ private static InputStream getResourceAsStream(String resourceName) throws FileNotFoundException {
+ InputStream is = Resources.class.getResourceAsStream(resourceName);
+ if (is == null) {
+ throw new FileNotFoundException(
+ "Cannot find resource '" + resourceName + "' on the class path.");
+ }
+ return is;
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java
new file mode 100644
index 00000000..46fe395d
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java
@@ -0,0 +1,74 @@
+/*
+ * 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.contrib.agent;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import io.opencensus.common.Internal;
+
+/**
+ * The {@code Settings} class provides access to user-configurable settings.
+ *
+ * @since 0.10
+ */
+public class Settings {
+
+ private static final String CONFIG_ROOT = "opencensus.contrib.agent";
+
+ private final Config config;
+
+ /** Creates agent settings. */
+ @Internal
+ @VisibleForTesting
+ public Settings(Config config) {
+ this.config = checkNotNull(config);
+ }
+
+ static Settings load() {
+ return new Settings(readConfig());
+ }
+
+ private static Config readConfig() {
+ Config config = ConfigFactory.load();
+ config.checkValid(ConfigFactory.defaultReference(), CONFIG_ROOT);
+
+ return config.getConfig(CONFIG_ROOT);
+ }
+
+ /**
+ * Checks whether a feature is enabled in the effective configuration.
+ *
+ * <p>A feature is identified by a path expression relative to {@link #CONFIG_ROOT}, such as
+ * {@code context-propagation.executor}. The feature is enabled iff the config element at the
+ * requested path has a child element {@code enabled} with a value of {@code true}, {@code on}, or
+ * {@code yes}.
+ *
+ * @param featurePath the feature's path expression
+ * @return true, if enabled, otherwise false
+ * @since 0.10
+ */
+ public boolean isEnabled(String featurePath) {
+ checkArgument(!Strings.isNullOrEmpty(featurePath));
+
+ return config.getConfig(featurePath).getBoolean("enabled");
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java
new file mode 100644
index 00000000..57d4efca
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java
@@ -0,0 +1,54 @@
+/*
+ * 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.contrib.agent.bootstrap;
+
+/**
+ * Strategy interface for accessing and manipulating the context.
+ *
+ * @since 0.6
+ */
+public interface ContextStrategy {
+
+ /**
+ * Wraps a {@link Runnable} so that it executes with the context that is associated with the
+ * current scope.
+ *
+ * @param runnable a {@link Runnable} object
+ * @return the wrapped {@link Runnable} object
+ * @since 0.6
+ */
+ Runnable wrapInCurrentContext(Runnable runnable);
+
+ /**
+ * Saves the context that is associated with the current scope.
+ *
+ * <p>The context will be attached when entering the specified thread's {@link Thread#run()}
+ * method.
+ *
+ * @param thread a {@link Thread} object
+ * @since 0.6
+ */
+ void saveContextForThread(Thread thread);
+
+ /**
+ * Attaches the context that was previously saved for the specified thread.
+ *
+ * @param thread a {@link Thread} object
+ * @since 0.6
+ */
+ void attachContextForThread(Thread thread);
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java
new file mode 100644
index 00000000..2e737be2
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.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.contrib.agent.bootstrap;
+
+/**
+ * {@code ContextTrampoline} provides methods for accessing and manipulating the context from
+ * instrumented bytecode.
+ *
+ * <p>{@code ContextTrampoline} avoids tight coupling with the concrete implementation of the
+ * context by accessing and manipulating the context through the {@link ContextStrategy} interface.
+ *
+ * <p>Both {@link ContextTrampoline} and {@link ContextStrategy} are loaded by the bootstrap
+ * classloader so that they can be used from classes loaded by the bootstrap classloader. A concrete
+ * implementation of {@link ContextStrategy} will be loaded by the system classloader. This allows
+ * for using the same context implementation as the instrumented application.
+ *
+ * <p>{@code ContextTrampoline} is implemented as a static class to allow for easy and fast use from
+ * instrumented bytecode. We cannot use dependency injection for the instrumented bytecode.
+ *
+ * @since 0.9
+ */
+// TODO(sebright): Fix the Checker Framework warnings.
+@SuppressWarnings("nullness")
+public final class ContextTrampoline {
+
+ // Not synchronized to avoid any synchronization costs after initialization.
+ // The agent is responsible for initializing this once (through #setContextStrategy) before any
+ // other method of this class is called.
+ private static ContextStrategy contextStrategy;
+
+ private ContextTrampoline() {}
+
+ /**
+ * Sets the concrete strategy for accessing and manipulating the context.
+ *
+ * <p>NB: The agent is responsible for setting the context strategy once before any other method
+ * of this class is called.
+ *
+ * @param contextStrategy the concrete strategy for accessing and manipulating the context
+ * @since 0.9
+ */
+ public static void setContextStrategy(ContextStrategy contextStrategy) {
+ if (ContextTrampoline.contextStrategy != null) {
+ throw new IllegalStateException("contextStrategy was already set");
+ }
+
+ if (contextStrategy == null) {
+ throw new NullPointerException("contextStrategy");
+ }
+
+ ContextTrampoline.contextStrategy = contextStrategy;
+ }
+
+ /**
+ * Wraps a {@link Runnable} so that it executes with the context that is associated with the
+ * current scope.
+ *
+ * @param runnable a {@link Runnable} object
+ * @return the wrapped {@link Runnable} object
+ * @see ContextStrategy#wrapInCurrentContext
+ * @since 0.9
+ */
+ public static Runnable wrapInCurrentContext(Runnable runnable) {
+ return contextStrategy.wrapInCurrentContext(runnable);
+ }
+
+ /**
+ * Saves the context that is associated with the current scope.
+ *
+ * <p>The context will be attached when entering the specified thread's {@link Thread#run()}
+ * method.
+ *
+ * @param thread a {@link Thread} object
+ * @since 0.9
+ */
+ public static void saveContextForThread(Thread thread) {
+ contextStrategy.saveContextForThread(thread);
+ }
+
+ /**
+ * Attaches the context that was previously saved for the specified thread.
+ *
+ * @param thread a {@link Thread} object
+ * @since 0.9
+ */
+ public static void attachContextForThread(Thread thread) {
+ contextStrategy.attachContextForThread(thread);
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java
new file mode 100644
index 00000000..363dbbdc
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.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.contrib.agent.bootstrap;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import java.io.Closeable;
+import javax.annotation.Nullable;
+
+/**
+ * Strategy interface for creating and manipulating trace spans.
+ *
+ * @since 0.9
+ */
+public interface TraceStrategy {
+
+ /**
+ * Starts a new span and sets it as the current span.
+ *
+ * <p>Enters the scope of code where the newly created {@code Span} is in the current Context, and
+ * returns an object that represents that scope. When the returned object is closed, the scope is
+ * exited, the previous Context is restored, and the newly created {@code Span} is ended using
+ * {@link io.opencensus.trace.Span#end}.
+ *
+ * <p>Callers must eventually close the returned object to avoid leaking the Context.
+ *
+ * <p>Supports the try-with-resource idiom.
+ *
+ * <p>NB: The return type of this method is intentionally {@link Closeable} and not the more
+ * specific {@link io.opencensus.common.Scope} because the latter would not be visible from
+ * classes loaded by the bootstrap classloader.
+ *
+ * @param spanName the name of the returned {@link io.opencensus.trace.Span}
+ * @return an object that defines a scope where the newly created {@code Span} will be set to the
+ * current Context
+ * @see io.opencensus.trace.Tracer#spanBuilder(java.lang.String)
+ * @see io.opencensus.trace.SpanBuilder#startScopedSpan()
+ * @since 0.9
+ */
+ @MustBeClosed
+ Closeable startScopedSpan(String spanName);
+
+ /**
+ * Ends the current span with a status derived from the given (optional) Throwable, and closes the
+ * given scope.
+ *
+ * @param scope an object representing the scope
+ * @param throwable an optional Throwable
+ * @since 0.9
+ */
+ void endScope(Closeable scope, @Nullable Throwable throwable);
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java
new file mode 100644
index 00000000..aeae2592
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java
@@ -0,0 +1,111 @@
+/*
+ * 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.contrib.agent.bootstrap;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import java.io.Closeable;
+import javax.annotation.Nullable;
+
+/**
+ * {@code TraceTrampoline} provides methods for creating and manipulating trace spans from
+ * instrumented bytecode.
+ *
+ * <p>{@code TraceTrampoline} avoids tight coupling with the concrete trace API through the {@link
+ * TraceStrategy} interface.
+ *
+ * <p>Both {@link TraceTrampoline} and {@link TraceStrategy} are loaded by the bootstrap classloader
+ * so that they can be used from classes loaded by the bootstrap classloader. A concrete
+ * implementation of {@link TraceStrategy} will be loaded by the system classloader. This allows for
+ * using the same trace API as the instrumented application.
+ *
+ * <p>{@code TraceTrampoline} is implemented as a static class to allow for easy and fast use from
+ * instrumented bytecode. We cannot use dependency injection for the instrumented bytecode.
+ *
+ * @since 0.9
+ */
+// TODO(sebright): Fix the Checker Framework warnings.
+@SuppressWarnings("nullness")
+public final class TraceTrampoline {
+
+ // Not synchronized to avoid any synchronization costs after initialization.
+ // The agent is responsible for initializing this once (through #setTraceStrategy) before any
+ // other method of this class is called.
+ private static TraceStrategy traceStrategy;
+
+ private TraceTrampoline() {}
+
+ /**
+ * Sets the concrete strategy for creating and manipulating trace spans.
+ *
+ * <p>NB: The agent is responsible for setting the trace strategy once before any other method of
+ * this class is called.
+ *
+ * @param traceStrategy the concrete strategy for creating and manipulating trace spans
+ * @since 0.9
+ */
+ public static void setTraceStrategy(TraceStrategy traceStrategy) {
+ if (TraceTrampoline.traceStrategy != null) {
+ throw new IllegalStateException("traceStrategy was already set");
+ }
+
+ if (traceStrategy == null) {
+ throw new NullPointerException("traceStrategy");
+ }
+
+ TraceTrampoline.traceStrategy = traceStrategy;
+ }
+
+ /**
+ * Starts a new span and sets it as the current span.
+ *
+ * <p>Enters the scope of code where the newly created {@code Span} is in the current Context, and
+ * returns an object that represents that scope. When the returned object is closed, the scope is
+ * exited, the previous Context is restored, and the newly created {@code Span} is ended using
+ * {@link io.opencensus.trace.Span#end}.
+ *
+ * <p>Callers must eventually close the returned object to avoid leaking the Context.
+ *
+ * <p>Supports the try-with-resource idiom.
+ *
+ * <p>NB: The return type of this method is intentionally {@link Closeable} and not the more
+ * specific {@link io.opencensus.common.Scope} because the latter would not be visible from
+ * classes loaded by the bootstrap classloader.
+ *
+ * @param spanName the name of the returned {@link io.opencensus.trace.Span}
+ * @return an object that defines a scope where the newly created {@code Span} will be set to the
+ * current Context
+ * @see io.opencensus.trace.Tracer#spanBuilder(String)
+ * @see io.opencensus.trace.SpanBuilder#startScopedSpan()
+ * @since 0.9
+ */
+ @MustBeClosed
+ public static Closeable startScopedSpan(String spanName) {
+ return traceStrategy.startScopedSpan(spanName);
+ }
+
+ /**
+ * Ends the current span with a status derived from the given (optional) Throwable, and closes the
+ * given scope.
+ *
+ * @param scope an object representing the scope
+ * @param throwable an optional Throwable
+ * @since 0.9
+ */
+ public static void endScope(Closeable scope, @Nullable Throwable throwable) {
+ traceStrategy.endScope(scope, throwable);
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java
new file mode 100644
index 00000000..f1363a26
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.contrib.agent.bootstrap;
+
+/**
+ * Contains classes that need to be loaded by the bootstrap classloader because they are used from
+ * classes loaded by the bootstrap classloader.
+ *
+ * <p>NB: Do not add direct dependencies on classes that are not loaded by the bootstrap
+ * classloader. Keep this package small.
+ */
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java
new file mode 100644
index 00000000..71e81270
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.contrib.agent.deps;
+
+/**
+ * Contains third party packages, such as Byte Buddy, Guava, etc., relocated here by the build
+ * process to avoid any conflicts of the agent's classes with the app's classes, which are loaded by
+ * the same classloader (the system classloader).
+ */
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java
new file mode 100644
index 00000000..8a6d8a6c
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java
@@ -0,0 +1,78 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import io.grpc.Context;
+import io.opencensus.contrib.agent.bootstrap.ContextStrategy;
+import java.lang.ref.WeakReference;
+
+/**
+ * Implementation of {@link ContextStrategy} for accessing and manipulating the {@link
+ * io.grpc.Context}.
+ */
+final class ContextStrategyImpl implements ContextStrategy {
+
+ /**
+ * Thread-safe mapping of {@link Thread}s to {@link Context}s, used for tunneling the caller's
+ * {@link Context} of {@link Thread#start()} to {@link Thread#run()}.
+ *
+ * <p>A thread is inserted into this map when {@link Thread#start()} is called, and removed when
+ * {@link Thread#run()} is called.
+ *
+ * <p>NB: {@link Thread#run()} is not guaranteed to be called after {@link Thread#start()}, for
+ * example when attempting to start a thread a second time. Therefore, threads are wrapped in
+ * {@link WeakReference}s so that this map does not prevent the garbage collection of otherwise
+ * unreferenced threads. Unreferenced threads will be automatically removed from the map by the
+ * routine cleanup of the underlying {@link Cache} implementation.
+ *
+ * <p>NB: A side-effect of {@link CacheBuilder#weakKeys()} is the use of identity ({@code ==})
+ * comparison to determine equality of threads. Identity comparison is required here because
+ * subclasses of {@link Thread} might override {@link Object#hashCode()} and {@link
+ * Object#equals(java.lang.Object)} with potentially broken implementations.
+ *
+ * <p>NB: Using thread IDs as keys was considered: It's unclear how to safely detect and cleanup
+ * otherwise unreferenced threads IDs from the map.
+ */
+ private final Cache<Thread, Context> savedContexts = CacheBuilder.newBuilder().weakKeys().build();
+
+ @Override
+ public Runnable wrapInCurrentContext(Runnable runnable) {
+ return Context.current().wrap(runnable);
+ }
+
+ @Override
+ public void saveContextForThread(Thread thread) {
+ savedContexts.put(thread, Context.current());
+ }
+
+ @Override
+ public void attachContextForThread(Thread thread) {
+ if (Thread.currentThread() == thread) {
+ Context context = savedContexts.getIfPresent(thread);
+ if (context != null) {
+ savedContexts.invalidate(thread);
+ // Work around findbugs warning. Context.attach() is marked as @CheckReturnValue so we need
+ // to check the return
+ // value here, otherwise findbugs will fail.
+ Preconditions.checkNotNull(context.attach(), "context.attach()");
+ }
+ }
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java
new file mode 100644
index 00000000..17a5b1d9
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.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.contrib.agent.instrumentation;
+
+import com.google.auto.service.AutoService;
+import io.opencensus.contrib.agent.Settings;
+import io.opencensus.contrib.agent.bootstrap.ContextStrategy;
+import io.opencensus.contrib.agent.bootstrap.ContextTrampoline;
+import net.bytebuddy.agent.builder.AgentBuilder;
+
+/**
+ * Initializes the {@link ContextTrampoline} with a concrete {@link ContextStrategy}.
+ *
+ * @since 0.9
+ */
+@AutoService(Instrumenter.class)
+public final class ContextTrampolineInitializer implements Instrumenter {
+
+ @Override
+ public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) {
+ // TODO(stschmidt): Gracefully handle the case of missing io.grpc.Context at runtime,
+ // maybe load the missing classes from a JAR that comes with the agent JAR.
+ ContextTrampoline.setContextStrategy(new ContextStrategyImpl());
+
+ return agentBuilder;
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java
new file mode 100644
index 00000000..1e1429ce
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java
@@ -0,0 +1,108 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
+import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf;
+import static net.bytebuddy.matcher.ElementMatchers.nameEndsWith;
+import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+
+import com.google.auto.service.AutoService;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.opencensus.contrib.agent.Settings;
+import io.opencensus.contrib.agent.bootstrap.ContextTrampoline;
+import java.util.concurrent.Executor;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType;
+import net.bytebuddy.matcher.ElementMatcher;
+import net.bytebuddy.utility.JavaModule;
+
+/**
+ * Propagates the context of the caller of {@link Executor#execute} to the submitted {@link
+ * Runnable}, just like the Microsoft .Net Framework propagates the <a
+ * href="https://msdn.microsoft.com/en-us/library/system.threading.executioncontext(v=vs.110).aspx">System.Threading.ExecutionContext</a>.
+ *
+ * @since 0.6
+ */
+@AutoService(Instrumenter.class)
+public final class ExecutorInstrumentation implements Instrumenter {
+
+ @Override
+ public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) {
+ checkNotNull(agentBuilder, "agentBuilder");
+ checkNotNull(settings, "settings");
+
+ if (!settings.isEnabled("context-propagation.executor")) {
+ return agentBuilder;
+ }
+
+ return agentBuilder.type(createMatcher()).transform(new Transformer());
+ }
+
+ private static class Transformer implements AgentBuilder.Transformer {
+
+ @Override
+ public DynamicType.Builder<?> transform(
+ DynamicType.Builder<?> builder,
+ TypeDescription typeDescription,
+ ClassLoader classLoader,
+ JavaModule module) {
+ return builder.visit(Advice.to(Execute.class).on(named("execute")));
+ }
+ }
+
+ private static ElementMatcher.Junction<TypeDescription> createMatcher() {
+ // This matcher matches implementations of Executor, but excludes CurrentContextExecutor and
+ // FixedContextExecutor from io.grpc.Context, which already propagate the context.
+ // TODO(stschmidt): As the executor implementation itself (e.g. ThreadPoolExecutor) is
+ // instrumented by the agent for automatic context propagation, CurrentContextExecutor could be
+ // turned into a no-op to avoid another unneeded context propagation. Likewise, when using
+ // FixedContextExecutor, the automatic context propagation added by the agent is unneeded.
+ return isSubTypeOf(Executor.class)
+ .and(not(isAbstract()))
+ .and(
+ not(
+ nameStartsWith("io.grpc.Context$")
+ .and(
+ nameEndsWith("CurrentContextExecutor")
+ .or(nameEndsWith("FixedContextExecutor")))));
+ }
+
+ private static class Execute {
+
+ /**
+ * Wraps a {@link Runnable} so that it executes with the context that is associated with the
+ * current scope.
+ *
+ * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode
+ * into Executor#execute.
+ *
+ * @see Advice
+ */
+ @Advice.OnMethodEnter
+ @SuppressWarnings(value = "UnusedAssignment")
+ @SuppressFBWarnings(value = {"DLS_DEAD_LOCAL_STORE", "UPM_UNCALLED_PRIVATE_METHOD"})
+ private static void enter(@Advice.Argument(value = 0, readOnly = false) Runnable runnable) {
+ runnable = ContextTrampoline.wrapInCurrentContext(runnable);
+ }
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java
new file mode 100644
index 00000000..5eb197ee
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import io.opencensus.contrib.agent.Settings;
+import net.bytebuddy.agent.builder.AgentBuilder;
+
+/**
+ * Interface for plug-ins that add bytecode instrumentation.
+ *
+ * @since 0.6
+ */
+public interface Instrumenter {
+
+ /**
+ * Adds bytecode instrumentation to the given {@link AgentBuilder}.
+ *
+ * @param agentBuilder an {@link AgentBuilder} object to which the additional instrumentation is
+ * added
+ * @param settings the configuration settings
+ * @return an {@link AgentBuilder} object having the additional instrumentation
+ * @since 0.10
+ */
+ AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings);
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java
new file mode 100644
index 00000000..b4beba8e
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java
@@ -0,0 +1,108 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import com.google.auto.service.AutoService;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.opencensus.contrib.agent.Settings;
+import io.opencensus.contrib.agent.bootstrap.ContextTrampoline;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType;
+import net.bytebuddy.utility.JavaModule;
+
+/**
+ * Propagates the context of the caller of {@link Thread#start} to the new thread, just like the
+ * Microsoft .Net Framework propagates the <a
+ * href="https://msdn.microsoft.com/en-us/library/system.threading.executioncontext(v=vs.110).aspx">System.Threading.ExecutionContext</a>.
+ *
+ * <p>NB: A similar effect could be achieved with {@link InheritableThreadLocal}, but the semantics
+ * are different: {@link InheritableThreadLocal} inherits values when the thread object is
+ * initialized as opposed to when {@link Thread#start()} is called.
+ *
+ * @since 0.6
+ */
+@AutoService(Instrumenter.class)
+public final class ThreadInstrumentation implements Instrumenter {
+
+ @Override
+ public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) {
+ checkNotNull(agentBuilder, "agentBuilder");
+ checkNotNull(settings, "settings");
+
+ if (!settings.isEnabled("context-propagation.thread")) {
+ return agentBuilder;
+ }
+
+ return agentBuilder.type(isSubTypeOf(Thread.class)).transform(new Transformer());
+ }
+
+ private static class Transformer implements AgentBuilder.Transformer {
+
+ @Override
+ public DynamicType.Builder<?> transform(
+ DynamicType.Builder<?> builder,
+ TypeDescription typeDescription,
+ ClassLoader classLoader,
+ JavaModule module) {
+ return builder
+ .visit(Advice.to(Start.class).on(named("start")))
+ .visit(Advice.to(Run.class).on(named("run")));
+ }
+ }
+
+ private static class Start {
+
+ /**
+ * Saves the context that is associated with the current scope.
+ *
+ * <p>The context will be attached when entering the thread's {@link Thread#run()} method.
+ *
+ * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode
+ * into Thread#start.
+ *
+ * @see Advice
+ */
+ @Advice.OnMethodEnter
+ @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD")
+ private static void enter(@Advice.This Thread thread) {
+ ContextTrampoline.saveContextForThread(thread);
+ }
+ }
+
+ private static class Run {
+
+ /**
+ * Attaches the context that was previously saved for this thread.
+ *
+ * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode
+ * into Thread#run.
+ *
+ * @see Advice
+ */
+ @Advice.OnMethodEnter
+ @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD")
+ private static void enter(@Advice.This Thread thread) {
+ ContextTrampoline.attachContextForThread(thread);
+ }
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java
new file mode 100644
index 00000000..139c10f3
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.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.contrib.agent.instrumentation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import io.opencensus.contrib.agent.bootstrap.TraceStrategy;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.Closeable;
+import java.io.IOException;
+import javax.annotation.Nullable;
+
+/** Implementation of {@link TraceStrategy} for creating and manipulating trace spans. */
+final class TraceStrategyImpl implements TraceStrategy {
+
+ @MustBeClosed
+ @Override
+ public Closeable startScopedSpan(String spanName) {
+ checkNotNull(spanName, "spanName");
+
+ return Tracing.getTracer()
+ .spanBuilder(spanName)
+ .setSampler(Samplers.alwaysSample())
+ .setRecordEvents(true)
+ .startScopedSpan();
+ }
+
+ @Override
+ public void endScope(Closeable scope, @Nullable Throwable throwable) {
+ checkNotNull(scope, "scope");
+
+ if (throwable != null) {
+ Tracing.getTracer()
+ .getCurrentSpan()
+ .setStatus(
+ Status.UNKNOWN.withDescription(
+ throwable.getMessage() == null
+ ? throwable.getClass().getSimpleName()
+ : throwable.getMessage()));
+ }
+
+ try {
+ scope.close();
+ } catch (IOException ex) {
+ // Ignore.
+ }
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java
new file mode 100644
index 00000000..4a68845c
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.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.contrib.agent.instrumentation;
+
+import com.google.auto.service.AutoService;
+import io.opencensus.contrib.agent.Settings;
+import io.opencensus.contrib.agent.bootstrap.TraceStrategy;
+import io.opencensus.contrib.agent.bootstrap.TraceTrampoline;
+import net.bytebuddy.agent.builder.AgentBuilder;
+
+/**
+ * Initializes the {@link TraceTrampoline} with a concrete {@link TraceStrategy}.
+ *
+ * @since 0.9
+ */
+@AutoService(Instrumenter.class)
+public final class TraceTrampolineInitializer implements Instrumenter {
+
+ @Override
+ public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) {
+ // TODO(stschmidt): Gracefully handle the case of missing trace API at runtime,
+ // maybe load the missing classes from a JAR that comes with the agent JAR.
+ TraceTrampoline.setTraceStrategy(new TraceStrategyImpl());
+
+ return agentBuilder;
+ }
+}
diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java
new file mode 100644
index 00000000..336f70b1
--- /dev/null
+++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java
@@ -0,0 +1,107 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import com.google.auto.service.AutoService;
+import com.google.errorprone.annotations.MustBeClosed;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.opencensus.contrib.agent.Settings;
+import io.opencensus.contrib.agent.bootstrap.TraceTrampoline;
+import java.io.Closeable;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType;
+import net.bytebuddy.utility.JavaModule;
+
+/**
+ * Wraps the execution of {@link java.net.URL#getContent()} in a trace span.
+ *
+ * <p>TODO(stschmidt): Replace this preliminary, java.net.URL-specific implementation with a
+ * generic, configurable implementation.
+ *
+ * @since 0.9
+ */
+@AutoService(Instrumenter.class)
+public final class UrlInstrumentation implements Instrumenter {
+
+ @Override
+ public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) {
+ checkNotNull(agentBuilder, "agentBuilder");
+ checkNotNull(settings, "settings");
+
+ if (!settings.isEnabled("trace.java.net.URL.getContent")) {
+ return agentBuilder;
+ }
+
+ return agentBuilder.type(named("java.net.URL")).transform(new Transformer());
+ }
+
+ private static class Transformer implements AgentBuilder.Transformer {
+
+ @Override
+ public DynamicType.Builder<?> transform(
+ DynamicType.Builder<?> builder,
+ TypeDescription typeDescription,
+ ClassLoader classLoader,
+ JavaModule module) {
+ return builder.visit(Advice.to(GetContent.class).on(named("getContent")));
+ }
+ }
+
+ private static class GetContent {
+
+ /**
+ * Starts a new span and sets it as the current span when entering the method.
+ *
+ * <p>The name of the new span is constructed from the name of the instrumented class and
+ * method. For example, in case of {@link java.net.URL#getContent()} the span name is {@code
+ * java.net.URL#getContent}.
+ *
+ * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode
+ * into Executor#execute.
+ *
+ * @see Advice
+ */
+ @Advice.OnMethodEnter
+ @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD")
+ @MustBeClosed
+ private static Closeable enter(@Advice.Origin("#t\\##m") String classAndMethodName) {
+ return TraceTrampoline.startScopedSpan(classAndMethodName);
+ }
+
+ /**
+ * Closes the current span and scope when exiting the method.
+ *
+ * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode
+ * into Executor#execute.
+ *
+ * <p>NB: By default, any {@link Throwable} thrown during the advice's execution is silently
+ * suppressed.
+ *
+ * @see Advice
+ */
+ @Advice.OnMethodExit(onThrowable = Throwable.class)
+ @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD")
+ private static void exit(@Advice.Enter Closeable scope, @Advice.Thrown Throwable throwable) {
+ TraceTrampoline.endScope(scope, throwable);
+ }
+ }
+}
diff --git a/contrib/agent/src/main/resources/reference.conf b/contrib/agent/src/main/resources/reference.conf
new file mode 100644
index 00000000..e1781248
--- /dev/null
+++ b/contrib/agent/src/main/resources/reference.conf
@@ -0,0 +1,23 @@
+# Reference configuration for the OpenCensus Agent for Java.
+
+opencensus.contrib.agent {
+
+ # Configuration settings related to automatic context propagation.
+ context-propagation {
+
+ # Enable/disable automatic context propagation for Executors.
+ executor.enabled = true
+
+ # Enable/disable automatic context propagation for Threads.
+ thread.enabled = true
+ }
+
+ # The "trace" section configures which Java methods the agent instruments for
+ # tracing.
+ trace {
+
+ java.net.URL.getContent {
+ enabled = true
+ }
+ }
+}
diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java
new file mode 100644
index 00000000..26eb696b
--- /dev/null
+++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.contrib.agent;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/** Unit tests for {@link Resources}. */
+@RunWith(MockitoJUnitRunner.class)
+public class ResourcesTest {
+
+ @Rule public final ExpectedException exception = ExpectedException.none();
+
+ @Mock private File mockFile;
+
+ @Test
+ public void getResourceAsTempFile_deleteOnExit() throws IOException {
+ Resources.getResourceAsTempFile("some_resource.txt", mockFile, new ByteArrayOutputStream());
+
+ verify(mockFile).deleteOnExit();
+ }
+
+ @Test
+ public void getResourceAsTempFile_contents() throws IOException {
+ File file = Resources.getResourceAsTempFile("some_resource.txt");
+
+ assertThat(Files.toString(file, Charsets.UTF_8)).isEqualTo("A resource!");
+ }
+
+ @Test
+ public void getResourceAsTempFile_empty() throws IOException {
+ exception.expect(IllegalArgumentException.class);
+
+ Resources.getResourceAsTempFile("");
+ }
+
+ @Test
+ public void getResourceAsTempFile_Missing() throws IOException {
+ exception.expect(FileNotFoundException.class);
+
+ Resources.getResourceAsTempFile("missing_resource.txt");
+ }
+
+ @Test
+ public void getResourceAsTempFile_WriteFailure() throws IOException {
+ OutputStream badOutputStream =
+ new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ throw new IOException("denied");
+ }
+ };
+
+ exception.expect(IOException.class);
+ exception.expectMessage("denied");
+
+ Resources.getResourceAsTempFile("some_resource.txt", mockFile, badOutputStream);
+ }
+}
diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java
new file mode 100644
index 00000000..4ed7120f
--- /dev/null
+++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.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.contrib.agent.bootstrap;
+
+import static org.mockito.Mockito.mock;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/** Unit tests for {@link ContextTrampoline}. */
+@RunWith(MockitoJUnitRunner.class)
+public class ContextTrampolineTest {
+
+ private static final ContextStrategy mockContextStrategy;
+
+ static {
+ mockContextStrategy = mock(ContextStrategy.class);
+ ContextTrampoline.setContextStrategy(mockContextStrategy);
+ }
+
+ @Rule public final ExpectedException exception = ExpectedException.none();
+
+ @Mock private Runnable runnable;
+
+ @Mock private Thread thread;
+
+ @Test
+ public void setContextStrategy_already_initialized() {
+ exception.expect(IllegalStateException.class);
+
+ ContextTrampoline.setContextStrategy(mockContextStrategy);
+ }
+
+ @Test
+ public void wrapInCurrentContext() {
+ ContextTrampoline.wrapInCurrentContext(runnable);
+
+ Mockito.verify(mockContextStrategy).wrapInCurrentContext(runnable);
+ }
+
+ @Test
+ public void saveContextForThread() {
+ ContextTrampoline.saveContextForThread(thread);
+
+ Mockito.verify(mockContextStrategy).saveContextForThread(thread);
+ }
+
+ @Test
+ public void attachContextForThread() {
+ ContextTrampoline.attachContextForThread(thread);
+
+ Mockito.verify(mockContextStrategy).attachContextForThread(thread);
+ }
+}
diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java
new file mode 100644
index 00000000..f1ca3500
--- /dev/null
+++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.contrib.agent.bootstrap;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+
+import java.io.Closeable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/** Unit tests for {@link TraceTrampoline}. */
+@RunWith(MockitoJUnitRunner.class)
+public class TraceTrampolineTest {
+
+ private static final TraceStrategy mockTraceStrategy = mock(TraceStrategy.class);
+
+ static {
+ TraceTrampoline.setTraceStrategy(mockTraceStrategy);
+ }
+
+ @Rule public final ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void setTraceStrategy_already_initialized() {
+ exception.expect(IllegalStateException.class);
+
+ TraceTrampoline.setTraceStrategy(mockTraceStrategy);
+ }
+
+ @Test
+ @SuppressWarnings("MustBeClosedChecker")
+ public void startScopedSpan() {
+ Closeable mockCloseable = mock(Closeable.class);
+ Mockito.when(mockTraceStrategy.startScopedSpan("test")).thenReturn(mockCloseable);
+
+ Closeable closeable = TraceTrampoline.startScopedSpan("test");
+
+ Mockito.verify(mockTraceStrategy).startScopedSpan("test");
+ assertThat(closeable).isSameAs(mockCloseable);
+ }
+}
diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java
new file mode 100644
index 00000000..75d8940e
--- /dev/null
+++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.typesafe.config.ConfigFactory;
+import io.opencensus.contrib.agent.Settings;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/** Unit tests for {@link ExecutorInstrumentation}. */
+@RunWith(MockitoJUnitRunner.class)
+public class ExecutorInstrumentationTest {
+
+ private final ExecutorInstrumentation instrumentation = new ExecutorInstrumentation();
+
+ private final AgentBuilder agentBuilder = new AgentBuilder.Default();
+
+ private static final String FEATURE = "context-propagation.executor";
+
+ @Test
+ public void instrument_disabled() {
+ Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false"));
+
+ AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings);
+
+ assertThat(agentBuilder2).isSameAs(agentBuilder);
+ }
+
+ @Test
+ public void instrument_enabled() {
+ Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true"));
+
+ AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings);
+
+ assertThat(agentBuilder2).isNotSameAs(agentBuilder);
+ }
+}
diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java
new file mode 100644
index 00000000..4585c37d
--- /dev/null
+++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.typesafe.config.ConfigFactory;
+import io.opencensus.contrib.agent.Settings;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/** Unit tests for {@link ThreadInstrumentation}. */
+@RunWith(MockitoJUnitRunner.class)
+public class ThreadInstrumentationTest {
+
+ private final ThreadInstrumentation instrumentation = new ThreadInstrumentation();
+
+ private final AgentBuilder agentBuilder = new AgentBuilder.Default();
+
+ private static final String FEATURE = "context-propagation.thread";
+
+ @Test
+ public void instrument_disabled() {
+ Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false"));
+
+ AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings);
+
+ assertThat(agentBuilder2).isSameAs(agentBuilder);
+ }
+
+ @Test
+ public void instrument_enabled() {
+ Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true"));
+
+ AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings);
+
+ assertThat(agentBuilder2).isNotSameAs(agentBuilder);
+ }
+}
diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java
new file mode 100644
index 00000000..3fa1249c
--- /dev/null
+++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.contrib.agent.instrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.typesafe.config.ConfigFactory;
+import io.opencensus.contrib.agent.Settings;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+
+/** Unit tests for {@link UrlInstrumentation}. */
+@RunWith(MockitoJUnitRunner.class)
+public class UrlInstrumentationTest {
+
+ private final UrlInstrumentation instrumentation = new UrlInstrumentation();
+
+ private final AgentBuilder agentBuilder = new AgentBuilder.Default();
+
+ private static final String FEATURE = "trace.java.net.URL.getContent";
+
+ @Test
+ public void instrument_disabled() {
+ Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false"));
+
+ AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings);
+
+ assertThat(agentBuilder2).isSameAs(agentBuilder);
+ }
+
+ @Test
+ public void instrument_enabled() {
+ Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true"));
+
+ AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings);
+
+ assertThat(agentBuilder2).isNotSameAs(agentBuilder);
+ }
+}
diff --git a/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt b/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt
new file mode 100644
index 00000000..07319bbd
--- /dev/null
+++ b/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt
@@ -0,0 +1 @@
+A resource! \ No newline at end of file
diff --git a/contrib/appengine_standard_util/README.md b/contrib/appengine_standard_util/README.md
new file mode 100644
index 00000000..3ff5a0ad
--- /dev/null
+++ b/contrib/appengine_standard_util/README.md
@@ -0,0 +1,35 @@
+# OpenCensus AppEngine Standard Util
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus AppEngine Standard Util for Java* is a collection of utilities for trace
+instrumentation when working with [AppEngine][appengine-url].
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-appengine-standard-util</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-appengine-standard-util:0.16.1'
+```
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-appengine-standard-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-appengine-standard-util
+[appengine-url]: https://appengine.google.com/
diff --git a/contrib/appengine_standard_util/build.gradle b/contrib/appengine_standard_util/build.gradle
new file mode 100644
index 00000000..a5c122a6
--- /dev/null
+++ b/contrib/appengine_standard_util/build.gradle
@@ -0,0 +1,52 @@
+description = 'OpenCensus AppEngine Standard Util'
+
+apply plugin: 'java'
+apply plugin: 'com.google.protobuf'
+
+def protocVersion = '3.5.1-1'
+
+buildscript {
+ repositories {
+ maven { url "https://plugins.gradle.org/m2/" }
+ }
+ dependencies {
+ classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.5"
+ }
+}
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.appengine_api,
+ libraries.guava,
+ libraries.protobuf
+
+ signature "org.codehaus.mojo.signature:java18:+@signature"
+}
+
+protobuf {
+ protoc {
+ // The artifact spec for the Protobuf Compiler
+ artifact = "com.google.protobuf:protoc:${protocVersion}"
+ }
+
+ generatedFilesBaseDir = "$projectDir/gen_gradle/src"
+
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ java {
+ option 'annotate_code'
+ }
+ }
+ }
+ }
+}
+
+clean {
+ delete protobuf.generatedFilesBaseDir
+}
diff --git a/contrib/appengine_standard_util/src/main/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtils.java b/contrib/appengine_standard_util/src/main/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtils.java
new file mode 100644
index 00000000..9fac951c
--- /dev/null
+++ b/contrib/appengine_standard_util/src/main/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtils.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2018, 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.contrib.appengine.standard.util;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.apphosting.api.CloudTraceContext;
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility class to convert between {@link io.opencensus.trace.SpanContext} and {@link
+ * CloudTraceContext}.
+ *
+ * @since 0.14
+ */
+public final class AppEngineCloudTraceContextUtils {
+ private static final byte[] INVALID_TRACE_ID =
+ TraceIdProto.newBuilder().setHi(0).setLo(0).build().toByteArray();
+ private static final long INVALID_SPAN_ID = 0L;
+ private static final long INVALID_TRACE_MASK = 0L;
+ private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+
+ @VisibleForTesting
+ static final CloudTraceContext INVALID_CLOUD_TRACE_CONTEXT =
+ new CloudTraceContext(INVALID_TRACE_ID, INVALID_SPAN_ID, INVALID_TRACE_MASK);
+
+ /**
+ * Converts AppEngine {@code CloudTraceContext} to {@code SpanContext}.
+ *
+ * @param cloudTraceContext the AppEngine {@code CloudTraceContext}.
+ * @return the converted {@code SpanContext}.
+ * @since 0.14
+ */
+ public static SpanContext fromCloudTraceContext(CloudTraceContext cloudTraceContext) {
+ checkNotNull(cloudTraceContext, "cloudTraceContext");
+
+ try {
+ // Extract the trace ID from the binary protobuf CloudTraceContext#traceId.
+ TraceIdProto traceIdProto = TraceIdProto.parseFrom(cloudTraceContext.getTraceId());
+ ByteBuffer traceIdBuf = ByteBuffer.allocate(TraceId.SIZE);
+ traceIdBuf.putLong(traceIdProto.getHi());
+ traceIdBuf.putLong(traceIdProto.getLo());
+ ByteBuffer spanIdBuf = ByteBuffer.allocate(SpanId.SIZE);
+ spanIdBuf.putLong(cloudTraceContext.getSpanId());
+
+ return SpanContext.create(
+ TraceId.fromBytes(traceIdBuf.array()),
+ SpanId.fromBytes(spanIdBuf.array()),
+ TraceOptions.builder().setIsSampled(cloudTraceContext.isTraceEnabled()).build(),
+ TRACESTATE_DEFAULT);
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Converts {@code SpanContext} to AppEngine {@code CloudTraceContext}.
+ *
+ * @param spanContext the {@code SpanContext}.
+ * @return the converted AppEngine {@code CloudTraceContext}.
+ * @since 0.14
+ */
+ public static CloudTraceContext toCloudTraceContext(SpanContext spanContext) {
+ checkNotNull(spanContext, "spanContext");
+
+ ByteBuffer traceIdBuf = ByteBuffer.wrap(spanContext.getTraceId().getBytes());
+ TraceIdProto traceIdProto =
+ TraceIdProto.newBuilder().setHi(traceIdBuf.getLong()).setLo(traceIdBuf.getLong()).build();
+ ByteBuffer spanIdBuf = ByteBuffer.wrap(spanContext.getSpanId().getBytes());
+
+ return new CloudTraceContext(
+ traceIdProto.toByteArray(),
+ spanIdBuf.getLong(),
+ spanContext.getTraceOptions().isSampled() ? 1L : 0L);
+ }
+
+ private AppEngineCloudTraceContextUtils() {}
+}
diff --git a/contrib/appengine_standard_util/src/main/proto/trace_id.proto b/contrib/appengine_standard_util/src/main/proto/trace_id.proto
new file mode 100644
index 00000000..35e2e087
--- /dev/null
+++ b/contrib/appengine_standard_util/src/main/proto/trace_id.proto
@@ -0,0 +1,10 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.opencensus.contrib.appengine.standard.util";
+option java_outer_classname = "TraceProto";
+
+message TraceIdProto {
+ fixed64 hi = 1;
+ fixed64 lo = 2;
+}
diff --git a/contrib/appengine_standard_util/src/test/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtilsTest.java b/contrib/appengine_standard_util/src/test/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtilsTest.java
new file mode 100644
index 00000000..dc53d8f3
--- /dev/null
+++ b/contrib/appengine_standard_util/src/test/java/io/opencensus/contrib/appengine/standard/util/AppEngineCloudTraceContextUtilsTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2018, 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.contrib.appengine.standard.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.appengine.standard.util.AppEngineCloudTraceContextUtils.INVALID_CLOUD_TRACE_CONTEXT;
+
+import com.google.apphosting.api.CloudTraceContext;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AppEngineCloudTraceContextUtils}. */
+@RunWith(JUnit4.class)
+public class AppEngineCloudTraceContextUtilsTest {
+ @Test
+ public void toFromSampledCloudTraceContext() {
+ CloudTraceContext cloudTraceContext =
+ new CloudTraceContext(
+ // Protobuf-encoded upper and lower 64 bits of the example trace ID
+ // fae1c6346b9cf9a272cb6504b5a10dcc/123456789.
+ new byte[] {
+ (byte) 0x09,
+ (byte) 0xa2,
+ (byte) 0xf9,
+ (byte) 0x9c,
+ (byte) 0x6b,
+ (byte) 0x34,
+ (byte) 0xc6,
+ (byte) 0xe1,
+ (byte) 0xfa,
+ (byte) 0x11,
+ (byte) 0xcc,
+ (byte) 0x0d,
+ (byte) 0xa1,
+ (byte) 0xb5,
+ (byte) 0x04,
+ (byte) 0x65,
+ (byte) 0xcb,
+ (byte) 0x72
+ },
+ Long.MIN_VALUE,
+ // Trace enabled.
+ 1L);
+
+ SpanContext spanContext =
+ AppEngineCloudTraceContextUtils.fromCloudTraceContext(cloudTraceContext);
+
+ assertThat(spanContext)
+ .isEqualTo(
+ SpanContext.create(
+ TraceId.fromLowerBase16("fae1c6346b9cf9a272cb6504b5a10dcc"),
+ SpanId.fromLowerBase16("8000000000000000"),
+ TraceOptions.builder().setIsSampled(true).build()));
+
+ // CloudTraceContext does not implement equals, so need to check every argument.
+ CloudTraceContext newCloudTraceContext =
+ AppEngineCloudTraceContextUtils.toCloudTraceContext(spanContext);
+ assertThat(newCloudTraceContext.getTraceId()).isEqualTo(cloudTraceContext.getTraceId());
+ assertThat(newCloudTraceContext.getSpanId()).isEqualTo(cloudTraceContext.getSpanId());
+ assertThat(newCloudTraceContext.getTraceMask()).isEqualTo(cloudTraceContext.getTraceMask());
+ }
+
+ @Test
+ public void toFromNotSampledCloudTraceContext() {
+ CloudTraceContext cloudTraceContext =
+ new CloudTraceContext(
+ // Protobuf-encoded upper and lower 64 bits of the example trace ID
+ // fae1c6346b9cf9a272cb6504b5a10dcc/123456789.
+ new byte[] {
+ (byte) 0x09,
+ (byte) 0xa2,
+ (byte) 0xf9,
+ (byte) 0x9c,
+ (byte) 0x6b,
+ (byte) 0x34,
+ (byte) 0xc6,
+ (byte) 0xe1,
+ (byte) 0xfa,
+ (byte) 0x11,
+ (byte) 0xcc,
+ (byte) 0x0d,
+ (byte) 0xa1,
+ (byte) 0xb5,
+ (byte) 0x04,
+ (byte) 0x65,
+ (byte) 0xcb,
+ (byte) 0x72
+ },
+ Long.MIN_VALUE,
+ // Trace disabled.
+ 0L);
+
+ SpanContext spanContext =
+ AppEngineCloudTraceContextUtils.fromCloudTraceContext(cloudTraceContext);
+
+ assertThat(spanContext)
+ .isEqualTo(
+ SpanContext.create(
+ TraceId.fromLowerBase16("fae1c6346b9cf9a272cb6504b5a10dcc"),
+ SpanId.fromLowerBase16("8000000000000000"),
+ TraceOptions.builder().setIsSampled(false).build()));
+
+ // CloudTraceContext does not implement equals, so need to check every argument.
+ assertThat(
+ cloudTraceContextEquals(
+ AppEngineCloudTraceContextUtils.toCloudTraceContext(spanContext),
+ cloudTraceContext))
+ .isTrue();
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void toCloudTraceContext_Null() {
+ AppEngineCloudTraceContextUtils.fromCloudTraceContext(null);
+ }
+
+ @Test
+ public void toCloudTraceContext_Invalid() {
+ assertThat(AppEngineCloudTraceContextUtils.fromCloudTraceContext(INVALID_CLOUD_TRACE_CONTEXT))
+ .isEqualTo(SpanContext.INVALID);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void fromCloudTraceContext_Null() {
+ AppEngineCloudTraceContextUtils.toCloudTraceContext(null);
+ }
+
+ @Test
+ public void fromCloudTraceContext_Invalid() {
+ assertThat(
+ cloudTraceContextEquals(
+ AppEngineCloudTraceContextUtils.toCloudTraceContext(SpanContext.INVALID),
+ INVALID_CLOUD_TRACE_CONTEXT))
+ .isTrue();
+ }
+
+ private static boolean cloudTraceContextEquals(CloudTraceContext obj1, CloudTraceContext obj2) {
+ return Arrays.equals(obj1.getTraceId(), obj2.getTraceId())
+ && obj1.getSpanId() == obj2.getSpanId()
+ && obj1.getTraceMask() == obj2.getTraceMask();
+ }
+}
diff --git a/contrib/dropwizard/README.md b/contrib/dropwizard/README.md
new file mode 100644
index 00000000..0010d005
--- /dev/null
+++ b/contrib/dropwizard/README.md
@@ -0,0 +1,112 @@
+# OpenCensus DropWizard Util for Java
+
+The *OpenCensus DropWizard Util for Java* provides an easy way to translate Dropwizard metrics to
+OpenCensus.
+
+## Quickstart
+
+### Prerequisites
+
+Assuming, you already have both the OpenCensus and Dropwizard client libraries setup and working
+inside your application.
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-dropwizard</artifactId>
+ <version>0.17.0</version>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-dropwizard:0.17.0'
+```
+
+### And the following code:
+
+```java
+import java.util.Collections;
+
+public class YourClass {
+ // Create registry for Dropwizard metrics.
+ static final com.codahale.metrics.MetricRegistry codahaleRegistry =
+ new com.codahale.metrics.MetricRegistry();
+
+ // Create a Dropwizard counter.
+ static final com.codahale.metrics.Counter requests = codahaleRegistry.counter("requests");
+
+ public static void main(String[] args) {
+
+ // Increment the requests.
+ requests.inc();
+
+ // Hook the Dropwizard registry into the OpenCensus registry
+ // via the DropWizardMetrics metric producer.
+ io.opencensus.metrics.Metrics.getExportComponent().getMetricProducerManager().add(
+ new io.opencensus.contrib.dropwizard.DropWizardMetrics(
+ Collections.singletonList(codahaleRegistry)));
+
+ }
+}
+```
+
+## Translation to OpenCensus Metrics
+
+This section describes how each of the DropWizard metrics translate into OpenCensus metrics.
+
+### DropWizard Counters
+
+Given a DropWizard Counter with name `cache_evictions`, the following values are reported:
+
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_cache_evictions_counter)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+(ex: Collected from Dropwizard (metric=cache_evictions, type=com.codahale.metrics.Counter))
+* type: GAUGE_INT64
+* unit: 1
+
+Note: OpenCensus's CUMULATIVE_INT64 type represent monotonically increasing values. Since
+DropWizard Counter goes up/down, it make sense to report them as OpenCensus GAUGE_INT64.
+
+### DropWizard Gauges
+
+Given a DropWizard Gauge with name `line_requests`, the following values are reported:
+
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_line_requests_gauge)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+* type: GAUGE_INT64 or GAUGE_DOUBLE
+* unit: 1
+
+Note: For simplicity, OpenCensus uses GAUGE_DOUBLE type for any Number and GAUGE_INT64
+type for Boolean values.
+
+### DropWizard Meters
+
+Given a DropWizard Meter with name `get_requests`, the following values are reported:
+
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_get_requests_meter)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+* type: CUMULATIVE_INT64
+* unit: 1
+
+### DropWizard Histograms
+
+Given a DropWizard Histogram with name `results`, the following values are reported:
+
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_results_histogram)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+* type: SUMMARY
+* unit: 1
+
+### DropWizard Timers
+
+Given a DropWizard Timer with name `requests`, the following values are reported:
+* name: codahale_<initial_metric_name>_<initial_type> (ex: codahale_requests_timer)
+* description: Collected from Dropwizard (metric=<metric_name>, type=<class_name>)
+* type: SUMMARY
+* unit: 1
diff --git a/contrib/dropwizard/build.gradle b/contrib/dropwizard/build.gradle
new file mode 100644
index 00000000..7da41cbe
--- /dev/null
+++ b/contrib/dropwizard/build.gradle
@@ -0,0 +1,17 @@
+description = 'OpenCensus dropwizard util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ project(':opencensus-impl-core')
+
+ compile libraries.dropwizard
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
diff --git a/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardMetrics.java b/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardMetrics.java
new file mode 100644
index 00000000..d9231837
--- /dev/null
+++ b/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardMetrics.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2018, 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.contrib.dropwizard;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Timer;
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.internal.DefaultVisibilityForTesting;
+import io.opencensus.internal.Utils;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.MetricProducer;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.Summary;
+import io.opencensus.metrics.export.Summary.Snapshot;
+import io.opencensus.metrics.export.Summary.Snapshot.ValueAtPercentile;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+
+/**
+ * Collects DropWizard metrics from a list {@link com.codahale.metrics.MetricRegistry}s.
+ *
+ * <p>A {@link io.opencensus.metrics.export.MetricProducer} that wraps a DropWizardMetrics.
+ *
+ * @since 0.17
+ */
+public class DropWizardMetrics extends MetricProducer {
+ @DefaultVisibilityForTesting static final String DEFAULT_UNIT = "1";
+ private final List<com.codahale.metrics.MetricRegistry> metricRegistryList;
+ private final Clock clock;
+ private final Timestamp cumulativeStartTimestamp;
+
+ /**
+ * Hook the Dropwizard registry into the OpenCensus registry.
+ *
+ * @param metricRegistryList a list of {@link com.codahale.metrics.MetricRegistry}s.
+ * @since 0.17
+ */
+ public DropWizardMetrics(List<com.codahale.metrics.MetricRegistry> metricRegistryList) {
+ Utils.checkNotNull(metricRegistryList, "metricRegistryList");
+ Utils.checkListElementNotNull(metricRegistryList, "metricRegistryList");
+ this.metricRegistryList = metricRegistryList;
+ clock = MillisClock.getInstance();
+ cumulativeStartTimestamp = clock.now();
+ }
+
+ /**
+ * Returns a {@code Metric} collected from {@link Gauge}.
+ *
+ * @param dropwizardName the metric name.
+ * @param gauge the gauge object to collect.
+ * @return a {@code Metric}.
+ */
+ @SuppressWarnings("rawtypes")
+ private @Nullable Metric collectGauge(String dropwizardName, Gauge gauge) {
+ String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "gauge");
+ String metricDescription = DropWizardUtils.generateFullMetricDescription(dropwizardName, gauge);
+
+ // Figure out which gauge instance and call the right method to get value
+ Type type;
+ Value value;
+
+ Object obj = gauge.getValue();
+ if (obj instanceof Number) {
+ type = Type.GAUGE_DOUBLE;
+ value = Value.doubleValue(((Number) obj).doubleValue());
+ } else if (obj instanceof Boolean) {
+ type = Type.GAUGE_INT64;
+ value = Value.longValue(((Boolean) obj) ? 1 : 0);
+ } else {
+ // Ignoring Gauge (gauge.getKey()) with unhandled type.
+ return null;
+ }
+
+ MetricDescriptor metricDescriptor =
+ MetricDescriptor.create(
+ metricName, metricDescription, DEFAULT_UNIT, type, Collections.<LabelKey>emptyList());
+ TimeSeries timeSeries =
+ TimeSeries.createWithOnePoint(
+ Collections.<LabelValue>emptyList(), Point.create(value, clock.now()), null);
+ return Metric.createWithOneTimeSeries(metricDescriptor, timeSeries);
+ }
+
+ /**
+ * Returns a {@code Metric} collected from {@link Counter}.
+ *
+ * @param dropwizardName the metric name.
+ * @param counter the counter object to collect.
+ * @return a {@code Metric}.
+ */
+ private Metric collectCounter(String dropwizardName, Counter counter) {
+ String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "counter");
+ String metricDescription =
+ DropWizardUtils.generateFullMetricDescription(dropwizardName, counter);
+
+ MetricDescriptor metricDescriptor =
+ MetricDescriptor.create(
+ metricName,
+ metricDescription,
+ DEFAULT_UNIT,
+ Type.GAUGE_INT64,
+ Collections.<LabelKey>emptyList());
+ TimeSeries timeSeries =
+ TimeSeries.createWithOnePoint(
+ Collections.<LabelValue>emptyList(),
+ Point.create(Value.longValue(counter.getCount()), clock.now()),
+ null);
+ return Metric.createWithOneTimeSeries(metricDescriptor, timeSeries);
+ }
+
+ /**
+ * Returns a {@code Metric} collected from {@link Meter}.
+ *
+ * @param dropwizardName the metric name.
+ * @param meter the meter object to collect
+ * @return a {@code Metric}.
+ */
+ private Metric collectMeter(String dropwizardName, Meter meter) {
+ String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "meter");
+ String metricDescription = DropWizardUtils.generateFullMetricDescription(dropwizardName, meter);
+
+ MetricDescriptor metricDescriptor =
+ MetricDescriptor.create(
+ metricName,
+ metricDescription,
+ DEFAULT_UNIT,
+ Type.CUMULATIVE_INT64,
+ Collections.<LabelKey>emptyList());
+ TimeSeries timeSeries =
+ TimeSeries.createWithOnePoint(
+ Collections.<LabelValue>emptyList(),
+ Point.create(Value.longValue(meter.getCount()), clock.now()),
+ null);
+
+ return Metric.createWithOneTimeSeries(metricDescriptor, timeSeries);
+ }
+
+ /**
+ * Returns a {@code Metric} collected from {@link Histogram}.
+ *
+ * @param dropwizardName the metric name.
+ * @param histogram the histogram object to collect
+ * @return a {@code Metric}.
+ */
+ private Metric collectHistogram(String dropwizardName, Histogram histogram) {
+ String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "histogram");
+ String metricDescription =
+ DropWizardUtils.generateFullMetricDescription(dropwizardName, histogram);
+ return collectSnapshotAndCount(
+ metricName, metricDescription, histogram.getSnapshot(), histogram.getCount());
+ }
+
+ /**
+ * Returns a {@code Metric} collected from {@link Timer}.
+ *
+ * @param dropwizardName the metric name.
+ * @param timer the timer object to collect
+ * @return a {@code Metric}.
+ */
+ private Metric collectTimer(String dropwizardName, Timer timer) {
+ String metricName = DropWizardUtils.generateFullMetricName(dropwizardName, "timer");
+ String metricDescription = DropWizardUtils.generateFullMetricDescription(dropwizardName, timer);
+ return collectSnapshotAndCount(
+ metricName, metricDescription, timer.getSnapshot(), timer.getCount());
+ }
+
+ /**
+ * Returns a {@code Metric} collected from {@link Snapshot}.
+ *
+ * @param metricName the metric name.
+ * @param metricDescription the metric description.
+ * @param codahaleSnapshot the snapshot object to collect
+ * @param count the value or count
+ * @return a {@code Metric}.
+ */
+ private Metric collectSnapshotAndCount(
+ String metricName,
+ String metricDescription,
+ com.codahale.metrics.Snapshot codahaleSnapshot,
+ long count) {
+ List<ValueAtPercentile> valueAtPercentiles =
+ Arrays.asList(
+ ValueAtPercentile.create(50.0, codahaleSnapshot.getMedian()),
+ ValueAtPercentile.create(75.0, codahaleSnapshot.get75thPercentile()),
+ ValueAtPercentile.create(98.0, codahaleSnapshot.get98thPercentile()),
+ ValueAtPercentile.create(99.0, codahaleSnapshot.get99thPercentile()),
+ ValueAtPercentile.create(99.9, codahaleSnapshot.get999thPercentile()));
+
+ Snapshot snapshot = Snapshot.create((long) codahaleSnapshot.size(), 0.0, valueAtPercentiles);
+ Point point =
+ Point.create(Value.summaryValue(Summary.create(count, 0.0, snapshot)), clock.now());
+
+ // TODO(mayurkale): OPTIMIZATION: Cache the MetricDescriptor objects.
+ MetricDescriptor metricDescriptor =
+ MetricDescriptor.create(
+ metricName,
+ metricDescription,
+ DEFAULT_UNIT,
+ Type.SUMMARY,
+ Collections.<LabelKey>emptyList());
+ TimeSeries timeSeries =
+ TimeSeries.createWithOnePoint(
+ Collections.<LabelValue>emptyList(), point, cumulativeStartTimestamp);
+
+ return Metric.createWithOneTimeSeries(metricDescriptor, timeSeries);
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public Collection<Metric> getMetrics() {
+ ArrayList<Metric> metrics = new ArrayList<Metric>();
+
+ for (com.codahale.metrics.MetricRegistry metricRegistry : metricRegistryList) {
+ for (Entry<String, Counter> counterEntry : metricRegistry.getCounters().entrySet()) {
+ metrics.add(collectCounter(counterEntry.getKey(), counterEntry.getValue()));
+ }
+
+ for (Entry<String, Gauge> gaugeEntry : metricRegistry.getGauges().entrySet()) {
+ Metric metric = collectGauge(gaugeEntry.getKey(), gaugeEntry.getValue());
+ if (metric != null) {
+ metrics.add(metric);
+ }
+ }
+
+ for (Entry<String, Meter> counterEntry : metricRegistry.getMeters().entrySet()) {
+ metrics.add(collectMeter(counterEntry.getKey(), counterEntry.getValue()));
+ }
+
+ for (Entry<String, Histogram> counterEntry : metricRegistry.getHistograms().entrySet()) {
+ metrics.add(collectHistogram(counterEntry.getKey(), counterEntry.getValue()));
+ }
+
+ for (Entry<String, Timer> counterEntry : metricRegistry.getTimers().entrySet()) {
+ metrics.add(collectTimer(counterEntry.getKey(), counterEntry.getValue()));
+ }
+ }
+
+ return metrics;
+ }
+}
diff --git a/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardUtils.java b/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardUtils.java
new file mode 100644
index 00000000..372e5c60
--- /dev/null
+++ b/contrib/dropwizard/src/main/java/io/opencensus/contrib/dropwizard/DropWizardUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018, 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.contrib.dropwizard;
+
+import com.codahale.metrics.Metric;
+
+/** Util methods for generating the metric name(unique) and description. */
+final class DropWizardUtils {
+ private static final String SOURCE = "codahale";
+ private static final char DELIMITER = '_';
+
+ /**
+ * Returns the metric name.
+ *
+ * @param name the initial metric name
+ * @param type the initial type of the metric.
+ * @return a string the unique metric name
+ */
+ static String generateFullMetricName(String name, String type) {
+ return SOURCE + DELIMITER + name + DELIMITER + type;
+ }
+
+ /**
+ * Returns the metric description.
+ *
+ * @param metricName the initial metric name
+ * @param metric the codahale metric class.
+ * @return a String the custom metric description
+ */
+ static String generateFullMetricDescription(String metricName, Metric metric) {
+ return "Collected from "
+ + SOURCE
+ + " (metric="
+ + metricName
+ + ", type="
+ + metric.getClass().getName()
+ + ")";
+ }
+
+ private DropWizardUtils() {}
+}
diff --git a/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardMetricsTest.java b/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardMetricsTest.java
new file mode 100644
index 00000000..2b41e9b8
--- /dev/null
+++ b/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardMetricsTest.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2018, 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.contrib.dropwizard;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.dropwizard.DropWizardMetrics.DEFAULT_UNIT;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.Timer;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Summary;
+import io.opencensus.metrics.export.Summary.Snapshot;
+import io.opencensus.metrics.export.Summary.Snapshot.ValueAtPercentile;
+import io.opencensus.metrics.export.Value;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DropWizardMetrics}. */
+@RunWith(JUnit4.class)
+public class DropWizardMetricsTest {
+
+ private com.codahale.metrics.MetricRegistry metricRegistry;
+ DropWizardMetrics dropWizardMetrics;
+
+ @Before
+ public void setUp() throws Exception {
+ metricRegistry = new com.codahale.metrics.MetricRegistry();
+ dropWizardMetrics = new DropWizardMetrics(Collections.singletonList((metricRegistry)));
+ }
+
+ @Test
+ public void collect() throws InterruptedException {
+
+ // create dropwizard metrics
+ Counter evictions = metricRegistry.counter("cache_evictions");
+ evictions.inc();
+ evictions.inc(3);
+ evictions.dec();
+ evictions.dec(2);
+
+ Gauge<Integer> integerGauge =
+ new Gauge<Integer>() {
+ @Override
+ public Integer getValue() {
+ return 1234;
+ }
+ };
+ metricRegistry.register("integer_gauge", integerGauge);
+
+ Gauge<Double> doubleGauge =
+ new Gauge<Double>() {
+ @Override
+ public Double getValue() {
+ return 1.234D;
+ }
+ };
+ metricRegistry.register("double_gauge", doubleGauge);
+
+ Gauge<Long> longGauge =
+ new Gauge<Long>() {
+ @Override
+ public Long getValue() {
+ return 1234L;
+ }
+ };
+ metricRegistry.register("long_gauge", longGauge);
+
+ Gauge<Float> floatGauge =
+ new Gauge<Float>() {
+ @Override
+ public Float getValue() {
+ return 0.1234F;
+ }
+ };
+ metricRegistry.register("float_gauge", floatGauge);
+
+ Gauge<Boolean> boolGauge =
+ new Gauge<Boolean>() {
+ @Override
+ public Boolean getValue() {
+ return Boolean.TRUE;
+ }
+ };
+ metricRegistry.register("boolean_gauge", boolGauge);
+
+ Meter getRequests = metricRegistry.meter("get_requests");
+ getRequests.mark();
+ getRequests.mark();
+
+ Histogram resultCounts = metricRegistry.histogram("result");
+ resultCounts.update(200);
+
+ Timer timer = metricRegistry.timer("requests");
+ Timer.Context context = timer.time();
+ Thread.sleep(1L);
+ context.stop();
+
+ ArrayList<Metric> metrics = new ArrayList<Metric>(dropWizardMetrics.getMetrics());
+ assertThat(metrics.size()).isEqualTo(9);
+
+ assertThat(metrics.get(0).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_cache_evictions_counter",
+ "Collected from codahale (metric=cache_evictions, "
+ + "type=com.codahale.metrics.Counter)",
+ DEFAULT_UNIT,
+ Type.GAUGE_INT64,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(0).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(0).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(0).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(0).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.longValue(1));
+ assertThat(metrics.get(0).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+ assertThat(metrics.get(1).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_boolean_gauge_gauge",
+ "Collected from codahale (metric=boolean_gauge, "
+ + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$5)",
+ DEFAULT_UNIT,
+ Type.GAUGE_INT64,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(1).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(1).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(1).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(1).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.longValue(1));
+ assertThat(metrics.get(1).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+ assertThat(metrics.get(2).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_double_gauge_gauge",
+ "Collected from codahale (metric=double_gauge, "
+ + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$2)",
+ DEFAULT_UNIT,
+ Type.GAUGE_DOUBLE,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(2).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(2).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(2).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(2).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.doubleValue(1.234));
+ assertThat(metrics.get(2).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+ assertThat(metrics.get(3).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_float_gauge_gauge",
+ "Collected from codahale (metric=float_gauge, "
+ + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$4)",
+ DEFAULT_UNIT,
+ Type.GAUGE_DOUBLE,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(3).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(3).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(3).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(3).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.doubleValue(0.1234000027179718));
+ assertThat(metrics.get(3).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+ assertThat(metrics.get(4).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_integer_gauge_gauge",
+ "Collected from codahale (metric=integer_gauge, "
+ + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$1)",
+ DEFAULT_UNIT,
+ Type.GAUGE_DOUBLE,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(4).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(4).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(4).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(4).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.doubleValue(1234.0));
+ assertThat(metrics.get(4).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+ assertThat(metrics.get(5).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_long_gauge_gauge",
+ "Collected from codahale (metric=long_gauge, "
+ + "type=io.opencensus.contrib.dropwizard.DropWizardMetricsTest$3)",
+ DEFAULT_UNIT,
+ Type.GAUGE_DOUBLE,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(5).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(5).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(5).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(5).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.doubleValue(1234.0));
+ assertThat(metrics.get(5).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+ assertThat(metrics.get(6).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_get_requests_meter",
+ "Collected from codahale (metric=get_requests, "
+ + "type=com.codahale.metrics.Meter)",
+ DEFAULT_UNIT,
+ Type.CUMULATIVE_INT64,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(6).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(6).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(6).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(6).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.longValue(2));
+ assertThat(metrics.get(6).getTimeSeriesList().get(0).getStartTimestamp()).isEqualTo(null);
+
+ assertThat(metrics.get(7).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_result_histogram",
+ "Collected from codahale (metric=result, " + "type=com.codahale.metrics.Histogram)",
+ DEFAULT_UNIT,
+ Type.SUMMARY,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(7).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(7).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(7).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(7).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(
+ Value.summaryValue(
+ Summary.create(
+ 1L,
+ 0.0,
+ Snapshot.create(
+ 1L,
+ 0.0,
+ Arrays.asList(
+ ValueAtPercentile.create(50.0, 200.0),
+ ValueAtPercentile.create(75.0, 200.0),
+ ValueAtPercentile.create(98.0, 200.0),
+ ValueAtPercentile.create(99.0, 200.0),
+ ValueAtPercentile.create(99.9, 200.0))))));
+ assertThat(metrics.get(7).getTimeSeriesList().get(0).getStartTimestamp())
+ .isInstanceOf(Timestamp.class);
+
+ assertThat(metrics.get(8).getMetricDescriptor())
+ .isEqualTo(
+ MetricDescriptor.create(
+ "codahale_requests_timer",
+ "Collected from codahale (metric=requests, " + "type=com.codahale.metrics.Timer)",
+ DEFAULT_UNIT,
+ Type.SUMMARY,
+ Collections.<LabelKey>emptyList()));
+ assertThat(metrics.get(8).getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metrics.get(8).getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(0);
+ assertThat(metrics.get(8).getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metrics.get(8).getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(
+ Value.summaryValue(
+ Summary.create(
+ 1L,
+ 0.0,
+ Snapshot.create(
+ 1L,
+ 0.0,
+ Arrays.asList(
+ ValueAtPercentile.create(50.0, timer.getSnapshot().getMedian()),
+ ValueAtPercentile.create(75.0, timer.getSnapshot().get75thPercentile()),
+ ValueAtPercentile.create(98.0, timer.getSnapshot().get98thPercentile()),
+ ValueAtPercentile.create(99.0, timer.getSnapshot().get99thPercentile()),
+ ValueAtPercentile.create(
+ 99.9, timer.getSnapshot().get999thPercentile()))))));
+
+ assertThat(metrics.get(8).getTimeSeriesList().get(0).getStartTimestamp())
+ .isInstanceOf(Timestamp.class);
+ }
+
+ @Test
+ public void empty_GetMetrics() {
+ assertThat(dropWizardMetrics.getMetrics()).isEmpty();
+ }
+}
diff --git a/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardUtilsTest.java b/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardUtilsTest.java
new file mode 100644
index 00000000..4dd27f29
--- /dev/null
+++ b/contrib/dropwizard/src/test/java/io/opencensus/contrib/dropwizard/DropWizardUtilsTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.contrib.dropwizard;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.codahale.metrics.Counter;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link DropWizardUtils}. */
+@RunWith(JUnit4.class)
+public class DropWizardUtilsTest {
+
+ @Test
+ public void generateFullMetricName() {
+ assertThat(DropWizardUtils.generateFullMetricName("requests", "gauge"))
+ .isEqualTo("codahale_requests_gauge");
+ }
+
+ @Test
+ public void generateFullMetricDescription() {
+ assertThat(DropWizardUtils.generateFullMetricDescription("Counter", new Counter()))
+ .isEqualTo("Collected from codahale (metric=Counter, type=com.codahale.metrics.Counter)");
+ }
+}
diff --git a/contrib/exemplar_util/README.md b/contrib/exemplar_util/README.md
new file mode 100644
index 00000000..1c9d62df
--- /dev/null
+++ b/contrib/exemplar_util/README.md
@@ -0,0 +1,36 @@
+# OpenCensus Exemplar Util
+
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Exemplar Util for Java* is a collection of utilities for recording Exemplars for
+OpenCensus stats.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-exemplar-util</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-exemplar-util:0.16.1'
+```
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-exemplar-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-exemplar-util
+
diff --git a/contrib/exemplar_util/build.gradle b/contrib/exemplar_util/build.gradle
new file mode 100644
index 00000000..9404b877
--- /dev/null
+++ b/contrib/exemplar_util/build.gradle
@@ -0,0 +1,15 @@
+description = 'OpenCensus Exemplar Util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/exemplar_util/src/main/java/io/opencensus/contrib/exemplar/util/ExemplarUtils.java b/contrib/exemplar_util/src/main/java/io/opencensus/contrib/exemplar/util/ExemplarUtils.java
new file mode 100644
index 00000000..7eb2116b
--- /dev/null
+++ b/contrib/exemplar_util/src/main/java/io/opencensus/contrib/exemplar/util/ExemplarUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018, 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.contrib.exemplar.util;
+
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import javax.annotation.Nullable;
+
+/**
+ * Utils for recording {@link Exemplar}s for OpenCensus stats.
+ *
+ * @since 0.16
+ */
+public final class ExemplarUtils {
+
+ /**
+ * Key for {@link TraceId} in the contextual information of an {@link Exemplar}.
+ *
+ * <p>For the {@code TraceId} value of this key, it is suggested to encode it in hex (base 16)
+ * lower case.
+ *
+ * @since 0.16
+ */
+ public static final String ATTACHMENT_KEY_TRACE_ID = "TraceId";
+
+ /**
+ * Key for {@link SpanId} in the contextual information of an {@link Exemplar}.
+ *
+ * <p>For the {@code SpanId} value of this key, it is suggested to encode it in hex (base 16)
+ * lower case.
+ *
+ * @since 0.16
+ */
+ public static final String ATTACHMENT_KEY_SPAN_ID = "SpanId";
+
+ /**
+ * Puts a {@link SpanContext} into the attachments of the given {@link MeasureMap}.
+ *
+ * <p>{@link TraceId} and {@link SpanId} of the {@link SpanContext} will be encoded in base 16
+ * lower case encoding.
+ *
+ * @param measureMap the {@code MeasureMap}
+ * @param spanContext the {@code SpanContext} to be put as attachments.
+ * @since 0.16
+ */
+ public static void putSpanContextAttachments(MeasureMap measureMap, SpanContext spanContext) {
+ checkNotNull(measureMap, "measureMap");
+ checkNotNull(spanContext, "spanContext");
+ measureMap.putAttachment(ATTACHMENT_KEY_TRACE_ID, spanContext.getTraceId().toLowerBase16());
+ measureMap.putAttachment(ATTACHMENT_KEY_SPAN_ID, spanContext.getSpanId().toLowerBase16());
+ }
+
+ // TODO: reuse this method from shared artifact.
+ private static <T> T checkNotNull(T reference, @Nullable Object errorMessage) {
+ if (reference == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+
+ private ExemplarUtils() {}
+}
diff --git a/contrib/exemplar_util/src/test/java/io/opencensus/contrib/exemplar/util/ExemplarUtilsTest.java b/contrib/exemplar_util/src/test/java/io/opencensus/contrib/exemplar/util/ExemplarUtilsTest.java
new file mode 100644
index 00000000..766f2c43
--- /dev/null
+++ b/contrib/exemplar_util/src/test/java/io/opencensus/contrib/exemplar/util/ExemplarUtilsTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2018, 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.contrib.exemplar.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.exemplar.util.ExemplarUtils.ATTACHMENT_KEY_SPAN_ID;
+import static io.opencensus.contrib.exemplar.util.ExemplarUtils.ATTACHMENT_KEY_TRACE_ID;
+
+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.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+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 ExemplarUtils}. */
+@RunWith(JUnit4.class)
+public class ExemplarUtilsTest {
+
+ private static final Random RANDOM = new Random(1234);
+ private static final TraceId TRACE_ID = TraceId.generateRandomId(RANDOM);
+ private static final SpanId SPAN_ID = SpanId.generateRandomId(RANDOM);
+ private static final SpanContext SPAN_CONTEXT =
+ SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT);
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void putSpanContext() {
+ FakeMeasureMap measureMap = new FakeMeasureMap();
+ ExemplarUtils.putSpanContextAttachments(measureMap, SPAN_CONTEXT);
+ assertThat(measureMap.attachments)
+ .containsExactly(
+ ATTACHMENT_KEY_TRACE_ID,
+ TRACE_ID.toLowerBase16(),
+ ATTACHMENT_KEY_SPAN_ID,
+ SPAN_ID.toLowerBase16());
+ }
+
+ @Test
+ public void putSpanContext_PreventNullMeasureMap() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("measureMap");
+ ExemplarUtils.putSpanContextAttachments(null, SPAN_CONTEXT);
+ }
+
+ @Test
+ public void putSpanContext_PreventNullSpanContext() {
+ FakeMeasureMap measureMap = new FakeMeasureMap();
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("spanContext");
+ ExemplarUtils.putSpanContextAttachments(measureMap, null);
+ }
+
+ private static final class FakeMeasureMap extends MeasureMap {
+
+ private final Map<String, String> attachments = new HashMap<String, String>();
+
+ @Override
+ public MeasureMap putAttachment(String key, String value) {
+ attachments.put(key, value);
+ return this;
+ }
+
+ @Override
+ public MeasureMap put(MeasureDouble measure, double value) {
+ return this;
+ }
+
+ @Override
+ public MeasureMap put(MeasureLong measure, long value) {
+ return this;
+ }
+
+ @Override
+ public void record() {}
+
+ @Override
+ public void record(TagContext tags) {}
+ }
+}
diff --git a/contrib/grpc_metrics/README.md b/contrib/grpc_metrics/README.md
new file mode 100644
index 00000000..b80cee23
--- /dev/null
+++ b/contrib/grpc_metrics/README.md
@@ -0,0 +1,5 @@
+# OpenCensus gRPC Metrics
+
+RPC measure and view constants used by gRPC.
+
+This library may be moved into gRPC in the future.
diff --git a/contrib/grpc_metrics/build.gradle b/contrib/grpc_metrics/build.gradle
new file mode 100644
index 00000000..a2de78d0
--- /dev/null
+++ b/contrib/grpc_metrics/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus gRPC Metrics'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstants.java b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstants.java
new file mode 100644
index 00000000..c09cfbfe
--- /dev/null
+++ b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstants.java
@@ -0,0 +1,495 @@
+/*
+ * 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.contrib.grpc.metrics;
+
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagKey;
+
+/**
+ * Constants for collecting rpc stats.
+ *
+ * @since 0.8
+ */
+public final class RpcMeasureConstants {
+
+ /**
+ * Tag key that represents a gRPC canonical status. Refer to
+ * https://github.com/grpc/grpc/blob/master/doc/statuscodes.md.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_STATUS} and {@link #GRPC_SERVER_STATUS}.
+ */
+ @Deprecated public static final TagKey RPC_STATUS = TagKey.create("canonical_status");
+
+ /**
+ * Tag key that represents a gRPC method.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_METHOD} and {@link #GRPC_SERVER_METHOD}.
+ */
+ @Deprecated public static final TagKey RPC_METHOD = TagKey.create("method");
+
+ /**
+ * Tag key that represents a client gRPC canonical status. Refer to
+ * https://github.com/grpc/grpc/blob/master/doc/statuscodes.md.
+ *
+ * <p>{@link #GRPC_CLIENT_STATUS} is set when an outgoing request finishes and is only available
+ * around metrics recorded at the end of the outgoing request.
+ *
+ * @since 0.13
+ */
+ public static final TagKey GRPC_CLIENT_STATUS = TagKey.create("grpc_client_status");
+
+ /**
+ * Tag key that represents a server gRPC canonical status. Refer to
+ * https://github.com/grpc/grpc/blob/master/doc/statuscodes.md.
+ *
+ * <p>{@link #GRPC_SERVER_STATUS} is set when an incoming request finishes and is only available
+ * around metrics recorded at the end of the incoming request.
+ *
+ * @since 0.13
+ */
+ public static final TagKey GRPC_SERVER_STATUS = TagKey.create("grpc_server_status");
+
+ /**
+ * Tag key that represents a client gRPC method.
+ *
+ * <p>{@link #GRPC_CLIENT_METHOD} is set when an outgoing request starts and is available in all
+ * the recorded metrics.
+ *
+ * @since 0.13
+ */
+ public static final TagKey GRPC_CLIENT_METHOD = TagKey.create("grpc_client_method");
+
+ /**
+ * Tag key that represents a server gRPC method.
+ *
+ * <p>{@link #GRPC_SERVER_METHOD} is set when an incoming request starts and is available in the
+ * context for the entire RPC call handling.
+ *
+ * @since 0.13
+ */
+ public static final TagKey GRPC_SERVER_METHOD = TagKey.create("grpc_server_method");
+
+ // Constants used to define the following Measures.
+
+ /**
+ * Unit string for byte.
+ *
+ * @since 0.8
+ */
+ private static final String BYTE = "By";
+
+ /**
+ * Unit string for count.
+ *
+ * @since 0.8
+ */
+ private static final String COUNT = "1";
+
+ /**
+ * Unit string for millisecond.
+ *
+ * @since 0.8
+ */
+ private static final String MILLISECOND = "ms";
+
+ // RPC client Measures.
+
+ /**
+ * {@link Measure} for gRPC client error counts.
+ *
+ * @since 0.8
+ * @deprecated because error counts can be computed on your metrics backend by totalling the
+ * different per-status values.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_CLIENT_ERROR_COUNT =
+ Measure.MeasureLong.create("grpc.io/client/error_count", "RPC Errors", COUNT);
+
+ /**
+ * {@link Measure} for gRPC client request bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_SENT_BYTES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_CLIENT_REQUEST_BYTES =
+ Measure.MeasureDouble.create("grpc.io/client/request_bytes", "Request bytes", BYTE);
+
+ /**
+ * {@link Measure} for gRPC client response bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_BYTES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_CLIENT_RESPONSE_BYTES =
+ Measure.MeasureDouble.create("grpc.io/client/response_bytes", "Response bytes", BYTE);
+
+ /**
+ * {@link Measure} for gRPC client roundtrip latency in milliseconds.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_ROUNDTRIP_LATENCY}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_CLIENT_ROUNDTRIP_LATENCY =
+ Measure.MeasureDouble.create(
+ "grpc.io/client/roundtrip_latency", "RPC roundtrip latency msec", MILLISECOND);
+
+ /**
+ * {@link Measure} for gRPC client server elapsed time in milliseconds.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_SERVER_LATENCY}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_CLIENT_SERVER_ELAPSED_TIME =
+ Measure.MeasureDouble.create(
+ "grpc.io/client/server_elapsed_time", "Server elapsed time in msecs", MILLISECOND);
+
+ /**
+ * {@link Measure} for gRPC client uncompressed request bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_SENT_BYTES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES =
+ Measure.MeasureDouble.create(
+ "grpc.io/client/uncompressed_request_bytes", "Uncompressed Request bytes", BYTE);
+
+ /**
+ * {@link Measure} for gRPC client uncompressed response bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_BYTES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES =
+ Measure.MeasureDouble.create(
+ "grpc.io/client/uncompressed_response_bytes", "Uncompressed Response bytes", BYTE);
+
+ /**
+ * {@link Measure} for number of started client RPCs.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_STARTED_RPCS}.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_CLIENT_STARTED_COUNT =
+ Measure.MeasureLong.create(
+ "grpc.io/client/started_count", "Number of client RPCs (streams) started", COUNT);
+
+ /**
+ * {@link Measure} for number of finished client RPCs.
+ *
+ * @since 0.8
+ * @deprecated since finished count can be inferred with a {@code Count} aggregation on {@link
+ * #GRPC_CLIENT_SERVER_LATENCY}.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_CLIENT_FINISHED_COUNT =
+ Measure.MeasureLong.create(
+ "grpc.io/client/finished_count", "Number of client RPCs (streams) finished", COUNT);
+
+ /**
+ * {@link Measure} for client RPC request message counts.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_SENT_MESSAGES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_CLIENT_REQUEST_COUNT =
+ Measure.MeasureLong.create(
+ "grpc.io/client/request_count", "Number of client RPC request messages", COUNT);
+
+ /**
+ * {@link Measure} for client RPC response message counts.
+ *
+ * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC}.
+ * @since 0.8
+ */
+ @Deprecated
+ public static final MeasureLong RPC_CLIENT_RESPONSE_COUNT =
+ Measure.MeasureLong.create(
+ "grpc.io/client/response_count", "Number of client RPC response messages", COUNT);
+
+ /**
+ * {@link Measure} for total bytes sent across all request messages per RPC.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble GRPC_CLIENT_SENT_BYTES_PER_RPC =
+ Measure.MeasureDouble.create(
+ "grpc.io/client/sent_bytes_per_rpc",
+ "Total bytes sent across all request messages per RPC",
+ BYTE);
+
+ /**
+ * {@link Measure} for total bytes received across all response messages per RPC.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble GRPC_CLIENT_RECEIVED_BYTES_PER_RPC =
+ Measure.MeasureDouble.create(
+ "grpc.io/client/received_bytes_per_rpc",
+ "Total bytes received across all response messages per RPC",
+ BYTE);
+
+ /**
+ * {@link Measure} for gRPC client roundtrip latency in milliseconds.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble GRPC_CLIENT_ROUNDTRIP_LATENCY =
+ Measure.MeasureDouble.create(
+ "grpc.io/client/roundtrip_latency",
+ "Time between first byte of request sent to last byte of response received, "
+ + "or terminal error.",
+ MILLISECOND);
+
+ /**
+ * {@link Measure} for number of messages sent in the RPC.
+ *
+ * @since 0.13
+ */
+ public static final MeasureLong GRPC_CLIENT_SENT_MESSAGES_PER_RPC =
+ Measure.MeasureLong.create(
+ "grpc.io/client/sent_messages_per_rpc", "Number of messages sent in the RPC", COUNT);
+
+ /**
+ * {@link Measure} for number of response messages received per RPC.
+ *
+ * @since 0.13
+ */
+ public static final MeasureLong GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC =
+ Measure.MeasureLong.create(
+ "grpc.io/client/received_messages_per_rpc",
+ "Number of response messages received per RPC",
+ COUNT);
+
+ /**
+ * {@link Measure} for gRPC server latency in milliseconds.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble GRPC_CLIENT_SERVER_LATENCY =
+ Measure.MeasureDouble.create(
+ "grpc.io/client/server_latency", "Server latency in msecs", MILLISECOND);
+
+ /**
+ * {@link Measure} for total number of client RPCs ever opened, including those that have not
+ * completed.
+ *
+ * @since 0.14
+ */
+ public static final MeasureLong GRPC_CLIENT_STARTED_RPCS =
+ Measure.MeasureLong.create(
+ "grpc.io/client/started_rpcs", "Number of started client RPCs.", COUNT);
+
+ // RPC server Measures.
+
+ /**
+ * {@link Measure} for gRPC server error counts.
+ *
+ * @since 0.8
+ * @deprecated because error counts can be computed on your metrics backend by totalling the
+ * different per-status values.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_SERVER_ERROR_COUNT =
+ Measure.MeasureLong.create("grpc.io/server/error_count", "RPC Errors", COUNT);
+
+ /**
+ * {@link Measure} for gRPC server request bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_BYTES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_SERVER_REQUEST_BYTES =
+ Measure.MeasureDouble.create("grpc.io/server/request_bytes", "Request bytes", BYTE);
+
+ /**
+ * {@link Measure} for gRPC server response bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SENT_BYTES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_SERVER_RESPONSE_BYTES =
+ Measure.MeasureDouble.create("grpc.io/server/response_bytes", "Response bytes", BYTE);
+
+ /**
+ * {@link Measure} for gRPC server elapsed time in milliseconds.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SERVER_LATENCY}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_SERVER_SERVER_ELAPSED_TIME =
+ Measure.MeasureDouble.create(
+ "grpc.io/server/server_elapsed_time", "Server elapsed time in msecs", MILLISECOND);
+
+ /**
+ * {@link Measure} for gRPC server latency in milliseconds.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SERVER_LATENCY}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_SERVER_SERVER_LATENCY =
+ Measure.MeasureDouble.create(
+ "grpc.io/server/server_latency", "Latency in msecs", MILLISECOND);
+
+ /**
+ * {@link Measure} for gRPC server uncompressed request bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_BYTES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES =
+ Measure.MeasureDouble.create(
+ "grpc.io/server/uncompressed_request_bytes", "Uncompressed Request bytes", BYTE);
+
+ /**
+ * {@link Measure} for gRPC server uncompressed response bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SENT_BYTES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureDouble RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES =
+ Measure.MeasureDouble.create(
+ "grpc.io/server/uncompressed_response_bytes", "Uncompressed Response bytes", BYTE);
+
+ /**
+ * {@link Measure} for number of started server RPCs.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_STARTED_RPCS}.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_SERVER_STARTED_COUNT =
+ Measure.MeasureLong.create(
+ "grpc.io/server/started_count", "Number of server RPCs (streams) started", COUNT);
+
+ /**
+ * {@link Measure} for number of finished server RPCs.
+ *
+ * @since 0.8
+ * @deprecated since finished count can be inferred with a {@code Count} aggregation on {@link
+ * #GRPC_SERVER_SERVER_LATENCY}.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_SERVER_FINISHED_COUNT =
+ Measure.MeasureLong.create(
+ "grpc.io/server/finished_count", "Number of server RPCs (streams) finished", COUNT);
+
+ /**
+ * {@link Measure} for server RPC request message counts.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_SERVER_REQUEST_COUNT =
+ Measure.MeasureLong.create(
+ "grpc.io/server/request_count", "Number of server RPC request messages", COUNT);
+
+ /**
+ * {@link Measure} for server RPC response message counts.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SENT_MESSAGES_PER_RPC}.
+ */
+ @Deprecated
+ public static final MeasureLong RPC_SERVER_RESPONSE_COUNT =
+ Measure.MeasureLong.create(
+ "grpc.io/server/response_count", "Number of server RPC response messages", COUNT);
+
+ /**
+ * {@link Measure} for total bytes sent across all response messages per RPC.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble GRPC_SERVER_SENT_BYTES_PER_RPC =
+ Measure.MeasureDouble.create(
+ "grpc.io/server/sent_bytes_per_rpc",
+ "Total bytes sent across all response messages per RPC",
+ BYTE);
+
+ /**
+ * {@link Measure} for total bytes received across all messages per RPC.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble GRPC_SERVER_RECEIVED_BYTES_PER_RPC =
+ Measure.MeasureDouble.create(
+ "grpc.io/server/received_bytes_per_rpc",
+ "Total bytes received across all messages per RPC",
+ BYTE);
+
+ /**
+ * {@link Measure} for number of messages sent in each RPC.
+ *
+ * @since 0.13
+ */
+ public static final MeasureLong GRPC_SERVER_SENT_MESSAGES_PER_RPC =
+ Measure.MeasureLong.create(
+ "grpc.io/server/sent_messages_per_rpc", "Number of messages sent in each RPC", COUNT);
+
+ /**
+ * {@link Measure} for number of messages received in each RPC.
+ *
+ * @since 0.13
+ */
+ public static final MeasureLong GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC =
+ Measure.MeasureLong.create(
+ "grpc.io/server/received_messages_per_rpc",
+ "Number of messages received in each RPC",
+ COUNT);
+
+ /**
+ * {@link Measure} for gRPC server latency in milliseconds.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble GRPC_SERVER_SERVER_LATENCY =
+ Measure.MeasureDouble.create(
+ "grpc.io/server/server_latency",
+ "Time between first byte of request received to last byte of response sent, "
+ + "or terminal error.",
+ MILLISECOND);
+
+ /**
+ * {@link Measure} for total number of server RPCs ever opened, including those that have not
+ * completed.
+ *
+ * @since 0.14
+ */
+ public static final MeasureLong GRPC_SERVER_STARTED_RPCS =
+ Measure.MeasureLong.create(
+ "grpc.io/server/started_rpcs", "Number of started server RPCs.", COUNT);
+
+ private RpcMeasureConstants() {}
+}
diff --git a/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViewConstants.java b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViewConstants.java
new file mode 100644
index 00000000..fbe1d58f
--- /dev/null
+++ b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViewConstants.java
@@ -0,0 +1,1339 @@
+/*
+ * 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.contrib.grpc.metrics;
+
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_METHOD;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_RECEIVED_BYTES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_SENT_MESSAGES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_SERVER_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_STARTED_RPCS;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_CLIENT_STATUS;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_METHOD;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_RECEIVED_BYTES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_SENT_BYTES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_SENT_MESSAGES_PER_RPC;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_SERVER_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_STARTED_RPCS;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_STATUS;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_ERROR_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_FINISHED_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_REQUEST_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_RESPONSE_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_RESPONSE_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_ROUNDTRIP_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_SERVER_ELAPSED_TIME;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_STARTED_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_METHOD;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_ERROR_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_FINISHED_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_REQUEST_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_RESPONSE_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_RESPONSE_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_SERVER_ELAPSED_TIME;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_SERVER_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_STARTED_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_STATUS;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.View;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Constants for exporting views on rpc stats.
+ *
+ * @since 0.8
+ */
+@SuppressWarnings("deprecation")
+public final class RpcViewConstants {
+
+ // Common histogram bucket boundaries for bytes received/sets Views.
+ @VisibleForTesting
+ static final List<Double> RPC_BYTES_BUCKET_BOUNDARIES =
+ Collections.unmodifiableList(
+ Arrays.asList(
+ 0.0,
+ 1024.0,
+ 2048.0,
+ 4096.0,
+ 16384.0,
+ 65536.0,
+ 262144.0,
+ 1048576.0,
+ 4194304.0,
+ 16777216.0,
+ 67108864.0,
+ 268435456.0,
+ 1073741824.0,
+ 4294967296.0));
+
+ // Common histogram bucket boundaries for latency and elapsed-time Views.
+ @VisibleForTesting
+ static final List<Double> RPC_MILLIS_BUCKET_BOUNDARIES =
+ Collections.unmodifiableList(
+ Arrays.asList(
+ 0.0, 0.01, 0.05, 0.1, 0.3, 0.6, 0.8, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0,
+ 16.0, 20.0, 25.0, 30.0, 40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0,
+ 300.0, 400.0, 500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
+ 100000.0));
+
+ static final List<Double> RPC_MILLIS_BUCKET_BOUNDARIES_DEPRECATED =
+ Collections.unmodifiableList(
+ Arrays.asList(
+ 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0, 25.0, 30.0, 40.0,
+ 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0, 300.0, 400.0, 500.0, 650.0,
+ 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0, 100000.0));
+
+ // Common histogram bucket boundaries for request/response count Views.
+ @VisibleForTesting
+ static final List<Double> RPC_COUNT_BUCKET_BOUNDARIES =
+ Collections.unmodifiableList(
+ Arrays.asList(
+ 0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0, 2048.0,
+ 4096.0, 8192.0, 16384.0, 32768.0, 65536.0));
+
+ // Use Aggregation.Mean to record sum and count stats at the same time.
+ @VisibleForTesting static final Aggregation MEAN = Aggregation.Mean.create();
+ @VisibleForTesting static final Aggregation COUNT = Count.create();
+
+ @VisibleForTesting
+ static final Aggregation AGGREGATION_WITH_BYTES_HISTOGRAM =
+ Distribution.create(BucketBoundaries.create(RPC_BYTES_BUCKET_BOUNDARIES));
+
+ @VisibleForTesting
+ static final Aggregation AGGREGATION_WITH_MILLIS_HISTOGRAM =
+ Distribution.create(BucketBoundaries.create(RPC_MILLIS_BUCKET_BOUNDARIES));
+
+ static final Aggregation AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED =
+ Distribution.create(BucketBoundaries.create(RPC_MILLIS_BUCKET_BOUNDARIES_DEPRECATED));
+
+ @VisibleForTesting
+ static final Aggregation AGGREGATION_WITH_COUNT_HISTOGRAM =
+ Distribution.create(BucketBoundaries.create(RPC_COUNT_BUCKET_BOUNDARIES));
+
+ @VisibleForTesting static final Duration MINUTE = Duration.create(60, 0);
+ @VisibleForTesting static final Duration HOUR = Duration.create(60 * 60, 0);
+
+ @VisibleForTesting
+ static final View.AggregationWindow CUMULATIVE = View.AggregationWindow.Cumulative.create();
+
+ @VisibleForTesting
+ static final View.AggregationWindow INTERVAL_MINUTE =
+ View.AggregationWindow.Interval.create(MINUTE);
+
+ @VisibleForTesting
+ static final View.AggregationWindow INTERVAL_HOUR = View.AggregationWindow.Interval.create(HOUR);
+
+ // Rpc client cumulative views.
+
+ /**
+ * Cumulative {@link View} for client RPC errors.
+ *
+ * @since 0.8
+ * @deprecated since error count measure is deprecated.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_ERROR_COUNT_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/error_count/cumulative"),
+ "RPC Errors",
+ RPC_CLIENT_ERROR_COUNT,
+ MEAN,
+ Arrays.asList(RPC_STATUS, RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for client roundtrip latency in milliseconds.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/roundtrip_latency/cumulative"),
+ "Latency in msecs",
+ RPC_CLIENT_ROUNDTRIP_LATENCY,
+ AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for client server elapsed time in milliseconds.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_SERVER_LATENCY_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_SERVER_ELAPSED_TIME_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/server_elapsed_time/cumulative"),
+ "Server elapsed time in msecs",
+ RPC_CLIENT_SERVER_ELAPSED_TIME,
+ AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for client request bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_REQUEST_BYTES_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/request_bytes/cumulative"),
+ "Request bytes",
+ RPC_CLIENT_REQUEST_BYTES,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for client response bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_RESPONSE_BYTES_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/response_bytes/cumulative"),
+ "Response bytes",
+ RPC_CLIENT_RESPONSE_BYTES,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for client uncompressed request bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/uncompressed_request_bytes/cumulative"),
+ "Uncompressed Request bytes",
+ RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for client uncompressed response bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/uncompressed_response_bytes/cumulative"),
+ "Uncompressed Response bytes",
+ RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for client request message counts.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_SENT_MESSAGES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_REQUEST_COUNT_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/request_count/cumulative"),
+ "Count of request messages per client RPC",
+ RPC_CLIENT_REQUEST_COUNT,
+ AGGREGATION_WITH_COUNT_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for client response message counts.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_RESPONSE_COUNT_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/response_count/cumulative"),
+ "Count of response messages per client RPC",
+ RPC_CLIENT_RESPONSE_COUNT,
+ AGGREGATION_WITH_COUNT_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for started client RPCs.
+ *
+ * @since 0.12
+ * @deprecated in favor of {@link #GRPC_CLIENT_STARTED_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/started_count/cumulative"),
+ "Number of started client RPCs",
+ RPC_CLIENT_STARTED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for finished client RPCs.
+ *
+ * @since 0.12
+ * @deprecated in favor of {@link #GRPC_CLIENT_COMPLETED_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_CLIENT_FINISHED_COUNT_CUMULATIVE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/finished_count/cumulative"),
+ "Number of finished client RPCs",
+ RPC_CLIENT_FINISHED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * {@link View} for client roundtrip latency in milliseconds.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/roundtrip_latency"),
+ "Latency in msecs",
+ GRPC_CLIENT_ROUNDTRIP_LATENCY,
+ AGGREGATION_WITH_MILLIS_HISTOGRAM,
+ Arrays.asList(GRPC_CLIENT_METHOD));
+
+ /**
+ * {@link View} for client server latency in milliseconds.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_CLIENT_SERVER_LATENCY_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/server_latency"),
+ "Server latency in msecs",
+ GRPC_CLIENT_SERVER_LATENCY,
+ AGGREGATION_WITH_MILLIS_HISTOGRAM,
+ Arrays.asList(GRPC_CLIENT_METHOD));
+
+ /**
+ * {@link View} for client sent bytes per RPC.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/sent_bytes_per_rpc"),
+ "Sent bytes per RPC",
+ GRPC_CLIENT_SENT_BYTES_PER_RPC,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(GRPC_CLIENT_METHOD));
+
+ /**
+ * {@link View} for client received bytes per RPC.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/received_bytes_per_rpc"),
+ "Received bytes per RPC",
+ GRPC_CLIENT_RECEIVED_BYTES_PER_RPC,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(GRPC_CLIENT_METHOD));
+
+ /**
+ * {@link View} for client sent messages per RPC.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_CLIENT_SENT_MESSAGES_PER_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/sent_messages_per_rpc"),
+ "Number of messages sent in the RPC",
+ GRPC_CLIENT_SENT_MESSAGES_PER_RPC,
+ AGGREGATION_WITH_COUNT_HISTOGRAM,
+ Arrays.asList(GRPC_CLIENT_METHOD));
+
+ /**
+ * {@link View} for client received messages per RPC.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/received_messages_per_rpc"),
+ "Number of response messages received per RPC",
+ GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC,
+ AGGREGATION_WITH_COUNT_HISTOGRAM,
+ Arrays.asList(GRPC_CLIENT_METHOD));
+
+ /**
+ * {@link View} for completed client RPCs.
+ *
+ * <p>This {@code View} uses measure {@code GRPC_CLIENT_ROUNDTRIP_LATENCY}, since completed RPCs
+ * can be inferred over any measure recorded once per RPC (since it's just a count aggregation
+ * over the measure). It would be unnecessary to use a separate "count" measure.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_CLIENT_COMPLETED_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/completed_rpcs"),
+ "Number of completed client RPCs",
+ GRPC_CLIENT_ROUNDTRIP_LATENCY,
+ COUNT,
+ Arrays.asList(GRPC_CLIENT_METHOD, GRPC_CLIENT_STATUS));
+
+ /**
+ * {@link View} for started client RPCs.
+ *
+ * @since 0.14
+ */
+ public static final View GRPC_CLIENT_STARTED_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/started_rpcs"),
+ "Number of started client RPCs",
+ GRPC_CLIENT_STARTED_RPCS,
+ COUNT,
+ Arrays.asList(GRPC_CLIENT_METHOD));
+
+ // Rpc server cumulative views.
+
+ /**
+ * Cumulative {@link View} for server RPC errors.
+ *
+ * @since 0.8
+ * @deprecated since error count measure is deprecated.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_ERROR_COUNT_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/error_count/cumulative"),
+ "RPC Errors",
+ RPC_SERVER_ERROR_COUNT,
+ MEAN,
+ Arrays.asList(RPC_STATUS, RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for server latency in milliseconds.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SERVER_LATENCY_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_SERVER_LATENCY_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/server_latency/cumulative"),
+ "Latency in msecs",
+ RPC_SERVER_SERVER_LATENCY,
+ AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for server elapsed time in milliseconds.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SERVER_LATENCY_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_SERVER_ELAPSED_TIME_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/elapsed_time/cumulative"),
+ "Server elapsed time in msecs",
+ RPC_SERVER_SERVER_ELAPSED_TIME,
+ AGGREGATION_WITH_MILLIS_HISTOGRAM_DEPRECATED,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for server request bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_REQUEST_BYTES_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/request_bytes/cumulative"),
+ "Request bytes",
+ RPC_SERVER_REQUEST_BYTES,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for server response bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_RESPONSE_BYTES_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/response_bytes/cumulative"),
+ "Response bytes",
+ RPC_SERVER_RESPONSE_BYTES,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for server uncompressed request bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/uncompressed_request_bytes/cumulative"),
+ "Uncompressed Request bytes",
+ RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for server uncompressed response bytes.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/uncompressed_response_bytes/cumulative"),
+ "Uncompressed Response bytes",
+ RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for server request message counts.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_REQUEST_COUNT_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/request_count/cumulative"),
+ "Count of request messages per server RPC",
+ RPC_SERVER_REQUEST_COUNT,
+ AGGREGATION_WITH_COUNT_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for server response message counts.
+ *
+ * @since 0.8
+ * @deprecated in favor of {@link #GRPC_SERVER_SENT_MESSAGES_PER_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_RESPONSE_COUNT_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/response_count/cumulative"),
+ "Count of response messages per server RPC",
+ RPC_SERVER_RESPONSE_COUNT,
+ AGGREGATION_WITH_COUNT_HISTOGRAM,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for started server RPCs.
+ *
+ * @since 0.12
+ * @deprecated in favor of {@link #GRPC_SERVER_STARTED_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/started_count/cumulative"),
+ "Number of started server RPCs",
+ RPC_SERVER_STARTED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * Cumulative {@link View} for finished server RPCs.
+ *
+ * @since 0.12
+ * @deprecated in favor of {@link #GRPC_SERVER_COMPLETED_RPC_VIEW}.
+ */
+ @Deprecated
+ public static final View RPC_SERVER_FINISHED_COUNT_CUMULATIVE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/finished_count/cumulative"),
+ "Number of finished server RPCs",
+ RPC_SERVER_FINISHED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ CUMULATIVE);
+
+ /**
+ * {@link View} for server server latency in milliseconds.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_SERVER_SERVER_LATENCY_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/server_latency"),
+ "Server latency in msecs",
+ GRPC_SERVER_SERVER_LATENCY,
+ AGGREGATION_WITH_MILLIS_HISTOGRAM,
+ Arrays.asList(GRPC_SERVER_METHOD));
+
+ /**
+ * {@link View} for server sent bytes per RPC.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/sent_bytes_per_rpc"),
+ "Sent bytes per RPC",
+ GRPC_SERVER_SENT_BYTES_PER_RPC,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(GRPC_SERVER_METHOD));
+
+ /**
+ * {@link View} for server received bytes per RPC.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/received_bytes_per_rpc"),
+ "Received bytes per RPC",
+ GRPC_SERVER_RECEIVED_BYTES_PER_RPC,
+ AGGREGATION_WITH_BYTES_HISTOGRAM,
+ Arrays.asList(GRPC_SERVER_METHOD));
+
+ /**
+ * {@link View} for server sent messages per RPC.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_SERVER_SENT_MESSAGES_PER_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/sent_messages_per_rpc"),
+ "Number of messages sent in each RPC",
+ GRPC_SERVER_SENT_MESSAGES_PER_RPC,
+ AGGREGATION_WITH_COUNT_HISTOGRAM,
+ Arrays.asList(GRPC_SERVER_METHOD));
+
+ /**
+ * {@link View} for server received messages per RPC.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/received_messages_per_rpc"),
+ "Number of response messages received in each RPC",
+ GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC,
+ AGGREGATION_WITH_COUNT_HISTOGRAM,
+ Arrays.asList(GRPC_SERVER_METHOD));
+
+ /**
+ * {@link View} for completed server RPCs.
+ *
+ * <p>This {@code View} uses measure {@code GRPC_SERVER_SERVER_LATENCY}, since completed RPCs can
+ * be inferred over any measure recorded once per RPC (since it's just a count aggregation over
+ * the measure). It would be unnecessary to use a separate "count" measure.
+ *
+ * @since 0.13
+ */
+ public static final View GRPC_SERVER_COMPLETED_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/completed_rpcs"),
+ "Number of completed server RPCs",
+ GRPC_SERVER_SERVER_LATENCY,
+ COUNT,
+ Arrays.asList(GRPC_SERVER_METHOD, GRPC_SERVER_STATUS));
+
+ /**
+ * {@link View} for started server RPCs.
+ *
+ * @since 0.14
+ */
+ public static final View GRPC_SERVER_STARTED_RPC_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/started_rpcs"),
+ "Number of started server RPCs",
+ GRPC_SERVER_STARTED_RPCS,
+ COUNT,
+ Arrays.asList(GRPC_SERVER_METHOD));
+
+ // Interval Stats
+
+ // RPC client interval views.
+
+ /**
+ * Minute {@link View} for client roundtrip latency in milliseconds.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/roundtrip_latency/minute"),
+ "Minute stats for latency in msecs",
+ RPC_CLIENT_ROUNDTRIP_LATENCY,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for client request bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/request_bytes/minute"),
+ "Minute stats for request size in bytes",
+ RPC_CLIENT_REQUEST_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for client response bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/response_bytes/minute"),
+ "Minute stats for response size in bytes",
+ RPC_CLIENT_RESPONSE_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for client RPC errors.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/error_count/minute"),
+ "Minute stats for rpc errors",
+ RPC_CLIENT_ERROR_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for client uncompressed request bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/uncompressed_request_bytes/minute"),
+ "Minute stats for uncompressed request size in bytes",
+ RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for client uncompressed response bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/uncompressed_response_bytes/minute"),
+ "Minute stats for uncompressed response size in bytes",
+ RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for client server elapsed time in milliseconds.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_SERVER_ELAPSED_TIME_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/server_elapsed_time/minute"),
+ "Minute stats for server elapsed time in msecs",
+ RPC_CLIENT_SERVER_ELAPSED_TIME,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for started client RPCs.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/started_count/minute"),
+ "Minute stats on the number of client RPCs started",
+ RPC_CLIENT_STARTED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for finished client RPCs.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/finished_count/minute"),
+ "Minute stats on the number of client RPCs finished",
+ RPC_CLIENT_FINISHED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for client request messages.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/request_count/minute"),
+ "Minute stats on the count of request messages per client RPC",
+ RPC_CLIENT_REQUEST_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for client response messages.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/response_count/minute"),
+ "Minute stats on the count of response messages per client RPC",
+ RPC_CLIENT_RESPONSE_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Hour {@link View} for client roundtrip latency in milliseconds.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/roundtrip_latency/hour"),
+ "Hour stats for latency in msecs",
+ RPC_CLIENT_ROUNDTRIP_LATENCY,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for client request bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/request_bytes/hour"),
+ "Hour stats for request size in bytes",
+ RPC_CLIENT_REQUEST_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for client response bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/response_bytes/hour"),
+ "Hour stats for response size in bytes",
+ RPC_CLIENT_RESPONSE_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for client RPC errors.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_ERROR_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/error_count/hour"),
+ "Hour stats for rpc errors",
+ RPC_CLIENT_ERROR_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for client uncompressed request bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/uncompressed_request_bytes/hour"),
+ "Hour stats for uncompressed request size in bytes",
+ RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for client uncompressed response bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/uncompressed_response_bytes/hour"),
+ "Hour stats for uncompressed response size in bytes",
+ RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for client server elapsed time in milliseconds.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_SERVER_ELAPSED_TIME_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/server_elapsed_time/hour"),
+ "Hour stats for server elapsed time in msecs",
+ RPC_CLIENT_SERVER_ELAPSED_TIME,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for started client RPCs.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_STARTED_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/started_count/hour"),
+ "Hour stats on the number of client RPCs started",
+ RPC_CLIENT_STARTED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for finished client RPCs.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/finished_count/hour"),
+ "Hour stats on the number of client RPCs finished",
+ RPC_CLIENT_FINISHED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for client request messages.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/request_count/hour"),
+ "Hour stats on the count of request messages per client RPC",
+ RPC_CLIENT_REQUEST_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for client response messages.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/client/response_count/hour"),
+ "Hour stats on the count of response messages per client RPC",
+ RPC_CLIENT_RESPONSE_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ // RPC server interval views.
+
+ /**
+ * Minute {@link View} for server latency in milliseconds.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/server_latency/minute"),
+ "Minute stats for server latency in msecs",
+ RPC_SERVER_SERVER_LATENCY,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for server request bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/request_bytes/minute"),
+ "Minute stats for request size in bytes",
+ RPC_SERVER_REQUEST_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for server response bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/response_bytes/minute"),
+ "Minute stats for response size in bytes",
+ RPC_SERVER_RESPONSE_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for server RPC errors.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_ERROR_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/error_count/minute"),
+ "Minute stats for rpc errors",
+ RPC_SERVER_ERROR_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for server uncompressed request bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/uncompressed_request_bytes/minute"),
+ "Minute stats for uncompressed request size in bytes",
+ RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for server uncompressed response bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/uncompressed_response_bytes/minute"),
+ "Minute stats for uncompressed response size in bytes",
+ RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for server elapsed time in milliseconds.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_SERVER_ELAPSED_TIME_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/server_elapsed_time/minute"),
+ "Minute stats for server elapsed time in msecs",
+ RPC_SERVER_SERVER_ELAPSED_TIME,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for started server RPCs.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_STARTED_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/started_count/minute"),
+ "Minute stats on the number of server RPCs started",
+ RPC_SERVER_STARTED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for finished server RPCs.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/finished_count/minute"),
+ "Minute stats on the number of server RPCs finished",
+ RPC_SERVER_FINISHED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for server request messages.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/request_count/minute"),
+ "Minute stats on the count of request messages per server RPC",
+ RPC_SERVER_REQUEST_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Minute {@link View} for server response messages.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/response_count/minute"),
+ "Minute stats on the count of response messages per server RPC",
+ RPC_SERVER_RESPONSE_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_MINUTE);
+
+ /**
+ * Hour {@link View} for server latency in milliseconds.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_SERVER_LATENCY_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/server_latency/hour"),
+ "Hour stats for server latency in msecs",
+ RPC_SERVER_SERVER_LATENCY,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for server request bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_REQUEST_BYTES_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/request_bytes/hour"),
+ "Hour stats for request size in bytes",
+ RPC_SERVER_REQUEST_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for server response bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/response_bytes/hour"),
+ "Hour stats for response size in bytes",
+ RPC_SERVER_RESPONSE_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for server RPC errors.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_ERROR_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/error_count/hour"),
+ "Hour stats for rpc errors",
+ RPC_SERVER_ERROR_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for server uncompressed request bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/uncompressed_request_bytes/hour"),
+ "Hour stats for uncompressed request size in bytes",
+ RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for server uncompressed response bytes.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/uncompressed_response_bytes/hour"),
+ "Hour stats for uncompressed response size in bytes",
+ RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for server elapsed time in milliseconds.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_SERVER_ELAPSED_TIME_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/server_elapsed_time/hour"),
+ "Hour stats for server elapsed time in msecs",
+ RPC_SERVER_SERVER_ELAPSED_TIME,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for started server RPCs.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_STARTED_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/started_count/hour"),
+ "Hour stats on the number of server RPCs started",
+ RPC_SERVER_STARTED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for finished server RPCs.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_FINISHED_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/finished_count/hour"),
+ "Hour stats on the number of server RPCs finished",
+ RPC_SERVER_FINISHED_COUNT,
+ COUNT,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for server request messages.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_REQUEST_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/request_count/hour"),
+ "Hour stats on the count of request messages per server RPC",
+ RPC_SERVER_REQUEST_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ /**
+ * Hour {@link View} for server response messages.
+ *
+ * @since 0.8
+ */
+ public static final View RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW =
+ View.create(
+ View.Name.create("grpc.io/server/response_count/hour"),
+ "Hour stats on the count of response messages per server RPC",
+ RPC_SERVER_RESPONSE_COUNT,
+ MEAN,
+ Arrays.asList(RPC_METHOD),
+ INTERVAL_HOUR);
+
+ private RpcViewConstants() {}
+}
diff --git a/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViews.java b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViews.java
new file mode 100644
index 00000000..ef06ba2b
--- /dev/null
+++ b/contrib/grpc_metrics/src/main/java/io/opencensus/contrib/grpc/metrics/RpcViews.java
@@ -0,0 +1,250 @@
+/*
+ * 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.contrib.grpc.metrics;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewManager;
+
+/**
+ * Helper class that allows users to register rpc views easily.
+ *
+ * @since 0.11
+ */
+@SuppressWarnings("deprecation")
+public final class RpcViews {
+ @VisibleForTesting
+ static final ImmutableSet<View> RPC_CUMULATIVE_VIEWS_SET =
+ ImmutableSet.of(
+ RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW,
+ RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW,
+ RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW,
+ RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_VIEW,
+ RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_VIEW,
+ RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_VIEW,
+ RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW,
+ RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW,
+ RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_VIEW,
+ RpcViewConstants.RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW,
+ RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_CUMULATIVE_VIEW,
+ RpcViewConstants.RPC_SERVER_ERROR_COUNT_VIEW,
+ RpcViewConstants.RPC_SERVER_SERVER_LATENCY_VIEW,
+ RpcViewConstants.RPC_SERVER_SERVER_ELAPSED_TIME_VIEW,
+ RpcViewConstants.RPC_SERVER_REQUEST_BYTES_VIEW,
+ RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_VIEW,
+ RpcViewConstants.RPC_SERVER_REQUEST_COUNT_VIEW,
+ RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_VIEW,
+ RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW,
+ RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW,
+ RpcViewConstants.RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW,
+ RpcViewConstants.RPC_SERVER_FINISHED_COUNT_CUMULATIVE_VIEW);
+
+ @VisibleForTesting
+ static final ImmutableSet<View> GRPC_CLIENT_VIEWS_SET =
+ ImmutableSet.of(
+ RpcViewConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW,
+ RpcViewConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW,
+ RpcViewConstants.GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW,
+ RpcViewConstants.GRPC_CLIENT_SENT_MESSAGES_PER_RPC_VIEW,
+ RpcViewConstants.GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC_VIEW,
+ RpcViewConstants.GRPC_CLIENT_SERVER_LATENCY_VIEW,
+ RpcViewConstants.GRPC_CLIENT_COMPLETED_RPC_VIEW,
+ RpcViewConstants.GRPC_CLIENT_STARTED_RPC_VIEW);
+
+ @VisibleForTesting
+ static final ImmutableSet<View> GRPC_SERVER_VIEWS_SET =
+ ImmutableSet.of(
+ RpcViewConstants.GRPC_SERVER_SERVER_LATENCY_VIEW,
+ RpcViewConstants.GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW,
+ RpcViewConstants.GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW,
+ RpcViewConstants.GRPC_SERVER_SENT_MESSAGES_PER_RPC_VIEW,
+ RpcViewConstants.GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC_VIEW,
+ RpcViewConstants.GRPC_SERVER_COMPLETED_RPC_VIEW,
+ RpcViewConstants.GRPC_SERVER_STARTED_RPC_VIEW);
+
+ @VisibleForTesting
+ static final ImmutableSet<View> RPC_INTERVAL_VIEWS_SET =
+ ImmutableSet.of(
+ RpcViewConstants.RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_ERROR_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_SERVER_ELAPSED_TIME_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_STARTED_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW,
+ RpcViewConstants.RPC_CLIENT_ERROR_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_STARTED_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_ERROR_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_SERVER_LATENCY_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_SERVER_ELAPSED_TIME_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_REQUEST_BYTES_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_REQUEST_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_STARTED_COUNT_HOUR_VIEW,
+ RpcViewConstants.RPC_SERVER_FINISHED_COUNT_HOUR_VIEW);
+
+ /**
+ * Registers all standard gRPC views.
+ *
+ * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+ *
+ * <p>This is equivalent with calling {@link #registerClientGrpcViews()} and {@link
+ * #registerServerGrpcViews()}.
+ *
+ * @since 0.13
+ */
+ public static void registerAllGrpcViews() {
+ registerAllGrpcViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerAllGrpcViews(ViewManager viewManager) {
+ registerClientGrpcViews(viewManager);
+ registerServerGrpcViews(viewManager);
+ }
+
+ /**
+ * Registers all standard client gRPC views.
+ *
+ * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+ *
+ * @since 0.16
+ */
+ public static void registerClientGrpcViews() {
+ registerClientGrpcViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerClientGrpcViews(ViewManager viewManager) {
+ for (View view : GRPC_CLIENT_VIEWS_SET) {
+ viewManager.registerView(view);
+ }
+ }
+
+ /**
+ * Registers all standard server gRPC views.
+ *
+ * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+ *
+ * @since 0.16
+ */
+ public static void registerServerGrpcViews() {
+ registerServerGrpcViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerServerGrpcViews(ViewManager viewManager) {
+ for (View view : GRPC_SERVER_VIEWS_SET) {
+ viewManager.registerView(view);
+ }
+ }
+
+ /**
+ * Registers all standard cumulative views.
+ *
+ * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+ *
+ * @since 0.11.0
+ * @deprecated in favor of {@link #registerAllGrpcViews()}. It is likely that there won't be stats
+ * for the old views, but you may still want to register the old views before they are
+ * completely removed.
+ */
+ @Deprecated
+ public static void registerAllCumulativeViews() {
+ registerAllCumulativeViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerAllCumulativeViews(ViewManager viewManager) {
+ for (View view : RPC_CUMULATIVE_VIEWS_SET) {
+ viewManager.registerView(view);
+ }
+ }
+
+ /**
+ * Registers all standard interval views.
+ *
+ * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+ *
+ * @since 0.11.0
+ * @deprecated because interval window is deprecated. There won't be interval views in the future.
+ */
+ @Deprecated
+ public static void registerAllIntervalViews() {
+ registerAllIntervalViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerAllIntervalViews(ViewManager viewManager) {
+ for (View view : RPC_INTERVAL_VIEWS_SET) {
+ viewManager.registerView(view);
+ }
+ }
+
+ /**
+ * Registers all views.
+ *
+ * <p>This is equivalent with calling {@link #registerAllCumulativeViews()} and {@link
+ * #registerAllIntervalViews()}.
+ *
+ * <p>It is recommended to call this method before doing any RPC call to avoid missing stats.
+ *
+ * @since 0.11.0
+ * @deprecated in favor of {@link #registerAllGrpcViews()}.
+ */
+ @Deprecated
+ public static void registerAllViews() {
+ registerAllViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerAllViews(ViewManager viewManager) {
+ registerAllCumulativeViews(viewManager);
+ registerAllIntervalViews(viewManager);
+ }
+
+ private RpcViews() {}
+}
diff --git a/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstantsTest.java b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstantsTest.java
new file mode 100644
index 00000000..107f0fea
--- /dev/null
+++ b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcMeasureConstantsTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.contrib.grpc.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link RpcMeasureConstants}. */
+@RunWith(JUnit4.class)
+public class RpcMeasureConstantsTest {
+
+ @Test
+ public void testConstants() {
+ assertThat(RpcMeasureConstants.RPC_STATUS).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_METHOD).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_METHOD).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_SERVER_METHOD).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_STATUS).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_SERVER_STATUS).isNotNull();
+
+ // Test client measurement descriptors.
+ assertThat(RpcMeasureConstants.RPC_CLIENT_ERROR_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_ROUNDTRIP_LATENCY).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_REQUEST_BYTES).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_RESPONSE_BYTES).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_REQUEST_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_RESPONSE_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_STARTED_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_FINISHED_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_CLIENT_SERVER_ELAPSED_TIME).isNotNull();
+
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_SENT_MESSAGES_PER_RPC).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_RECEIVED_BYTES_PER_RPC).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_SERVER_LATENCY).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_CLIENT_STARTED_RPCS).isNotNull();
+
+ // Test server measurement descriptors.
+ assertThat(RpcMeasureConstants.RPC_SERVER_ERROR_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_REQUEST_BYTES).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_RESPONSE_BYTES).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_SERVER_LATENCY).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_REQUEST_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_RESPONSE_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_STARTED_COUNT).isNotNull();
+ assertThat(RpcMeasureConstants.RPC_SERVER_FINISHED_COUNT).isNotNull();
+
+ assertThat(RpcMeasureConstants.GRPC_SERVER_SENT_BYTES_PER_RPC).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_SERVER_SENT_MESSAGES_PER_RPC).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_SERVER_RECEIVED_BYTES_PER_RPC).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_SERVER_SERVER_LATENCY).isNotNull();
+ assertThat(RpcMeasureConstants.GRPC_SERVER_STARTED_RPCS).isNotNull();
+ }
+}
diff --git a/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewConstantsTest.java b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewConstantsTest.java
new file mode 100644
index 00000000..6f8b5165
--- /dev/null
+++ b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewConstantsTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.contrib.grpc.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link RpcViewConstants}. */
+@RunWith(JUnit4.class)
+public final class RpcViewConstantsTest {
+
+ @Test
+ public void testConstants() {
+
+ // Test bucket boundaries.
+ assertThat(RpcViewConstants.RPC_BYTES_BUCKET_BOUNDARIES)
+ .containsExactly(
+ 0.0,
+ 1024.0,
+ 2048.0,
+ 4096.0,
+ 16384.0,
+ 65536.0,
+ 262144.0,
+ 1048576.0,
+ 4194304.0,
+ 16777216.0,
+ 67108864.0,
+ 268435456.0,
+ 1073741824.0,
+ 4294967296.0)
+ .inOrder();
+ assertThat(RpcViewConstants.RPC_MILLIS_BUCKET_BOUNDARIES)
+ .containsExactly(
+ 0.0, 0.01, 0.05, 0.1, 0.3, 0.6, 0.8, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0,
+ 16.0, 20.0, 25.0, 30.0, 40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0,
+ 300.0, 400.0, 500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
+ 100000.0)
+ .inOrder();
+ assertThat(RpcViewConstants.RPC_COUNT_BUCKET_BOUNDARIES)
+ .containsExactly(
+ 0.0, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0, 1024.0, 2048.0, 4096.0,
+ 8192.0, 16384.0, 32768.0, 65536.0)
+ .inOrder();
+
+ // Test Aggregations
+ assertThat(RpcViewConstants.MEAN).isEqualTo(Mean.create());
+ assertThat(RpcViewConstants.COUNT).isEqualTo(Count.create());
+ assertThat(RpcViewConstants.AGGREGATION_WITH_BYTES_HISTOGRAM)
+ .isEqualTo(
+ Distribution.create(
+ BucketBoundaries.create(RpcViewConstants.RPC_BYTES_BUCKET_BOUNDARIES)));
+ assertThat(RpcViewConstants.AGGREGATION_WITH_MILLIS_HISTOGRAM)
+ .isEqualTo(
+ Distribution.create(
+ BucketBoundaries.create(RpcViewConstants.RPC_MILLIS_BUCKET_BOUNDARIES)));
+ assertThat(RpcViewConstants.AGGREGATION_WITH_COUNT_HISTOGRAM)
+ .isEqualTo(
+ Distribution.create(
+ BucketBoundaries.create(RpcViewConstants.RPC_COUNT_BUCKET_BOUNDARIES)));
+
+ // Test Duration and Window
+ assertThat(RpcViewConstants.MINUTE).isEqualTo(Duration.create(60, 0));
+ assertThat(RpcViewConstants.HOUR).isEqualTo(Duration.create(60 * 60, 0));
+ assertThat(RpcViewConstants.CUMULATIVE).isEqualTo(Cumulative.create());
+ assertThat(RpcViewConstants.INTERVAL_MINUTE)
+ .isEqualTo(Interval.create(RpcViewConstants.MINUTE));
+ assertThat(RpcViewConstants.INTERVAL_HOUR).isEqualTo(Interval.create(RpcViewConstants.HOUR));
+
+ // Test client distribution view descriptors.
+ assertThat(RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_VIEW).isNotNull();
+
+ assertThat(RpcViewConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_CLIENT_RECEIVED_BYTES_PER_RPC_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_CLIENT_SENT_MESSAGES_PER_RPC_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_CLIENT_RECEIVED_MESSAGES_PER_RPC_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_CLIENT_SERVER_LATENCY_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_CLIENT_STARTED_RPC_VIEW).isNotNull();
+
+ // Test server distribution view descriptors.
+ assertThat(RpcViewConstants.RPC_SERVER_ERROR_COUNT_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_SERVER_LATENCY_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_REQUEST_BYTES_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_REQUEST_COUNT_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_VIEW).isNotNull();
+
+ assertThat(RpcViewConstants.GRPC_SERVER_SENT_BYTES_PER_RPC_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_SERVER_RECEIVED_BYTES_PER_RPC_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_SERVER_SENT_MESSAGES_PER_RPC_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_SERVER_RECEIVED_MESSAGES_PER_RPC_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_SERVER_SERVER_LATENCY_VIEW).isNotNull();
+ assertThat(RpcViewConstants.GRPC_SERVER_STARTED_RPC_VIEW).isNotNull();
+
+ // Test client interval view descriptors.
+ assertThat(RpcViewConstants.RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW).isNotNull();
+
+ assertThat(RpcViewConstants.RPC_CLIENT_ERROR_COUNT_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_STARTED_COUNT_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_SERVER_ELAPSED_TIME_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW).isNotNull();
+
+ // Test server interval view descriptors.
+ assertThat(RpcViewConstants.RPC_SERVER_ERROR_COUNT_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_STARTED_COUNT_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW).isNotNull();
+
+ assertThat(RpcViewConstants.RPC_SERVER_ERROR_COUNT_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_SERVER_LATENCY_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_REQUEST_BYTES_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_STARTED_COUNT_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_FINISHED_COUNT_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_REQUEST_COUNT_HOUR_VIEW).isNotNull();
+ assertThat(RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW).isNotNull();
+ }
+}
diff --git a/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewsTest.java b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewsTest.java
new file mode 100644
index 00000000..a908629f
--- /dev/null
+++ b/contrib/grpc_metrics/src/test/java/io/opencensus/contrib/grpc/metrics/RpcViewsTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.contrib.grpc.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link RpcViews}. */
+@RunWith(JUnit4.class)
+public class RpcViewsTest {
+
+ @Test
+ public void registerCumulative() {
+ FakeViewManager fakeViewManager = new FakeViewManager();
+ RpcViews.registerAllCumulativeViews(fakeViewManager);
+ assertThat(fakeViewManager.getRegisteredViews())
+ .containsExactlyElementsIn(RpcViews.RPC_CUMULATIVE_VIEWS_SET);
+ }
+
+ @Test
+ public void registerInterval() {
+ FakeViewManager fakeViewManager = new FakeViewManager();
+ RpcViews.registerAllIntervalViews(fakeViewManager);
+ assertThat(fakeViewManager.getRegisteredViews())
+ .containsExactlyElementsIn(RpcViews.RPC_INTERVAL_VIEWS_SET);
+ }
+
+ @Test
+ public void registerAll() {
+ FakeViewManager fakeViewManager = new FakeViewManager();
+ RpcViews.registerAllViews(fakeViewManager);
+ assertThat(fakeViewManager.getRegisteredViews())
+ .containsExactlyElementsIn(
+ ImmutableSet.builder()
+ .addAll(RpcViews.RPC_CUMULATIVE_VIEWS_SET)
+ .addAll(RpcViews.RPC_INTERVAL_VIEWS_SET)
+ .build());
+ }
+
+ @Test
+ public void registerAllGrpcViews() {
+ FakeViewManager fakeViewManager = new FakeViewManager();
+ RpcViews.registerAllGrpcViews(fakeViewManager);
+ assertThat(fakeViewManager.getRegisteredViews())
+ .containsExactlyElementsIn(
+ ImmutableSet.builder()
+ .addAll(RpcViews.GRPC_CLIENT_VIEWS_SET)
+ .addAll(RpcViews.GRPC_SERVER_VIEWS_SET)
+ .build());
+ }
+
+ @Test
+ public void registerClientGrpcViews() {
+ FakeViewManager fakeViewManager = new FakeViewManager();
+ RpcViews.registerClientGrpcViews(fakeViewManager);
+ assertThat(fakeViewManager.getRegisteredViews())
+ .containsExactlyElementsIn(RpcViews.GRPC_CLIENT_VIEWS_SET);
+ }
+
+ @Test
+ public void registerServerGrpcViews() {
+ FakeViewManager fakeViewManager = new FakeViewManager();
+ RpcViews.registerServerGrpcViews(fakeViewManager);
+ assertThat(fakeViewManager.getRegisteredViews())
+ .containsExactlyElementsIn(RpcViews.GRPC_SERVER_VIEWS_SET);
+ }
+
+ // TODO(bdrutu): Test with reflection that all defined gRPC views are registered.
+
+ private static final class FakeViewManager extends ViewManager {
+ private final Map<View.Name, View> registeredViews = Maps.newHashMap();
+
+ private FakeViewManager() {}
+
+ @Override
+ public void registerView(View view) {
+ registeredViews.put(view.getName(), view);
+ }
+
+ @Nullable
+ @Override
+ public ViewData getView(View.Name view) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<View> getAllExportedViews() {
+ throw new UnsupportedOperationException();
+ }
+
+ private Collection<View> getRegisteredViews() {
+ return registeredViews.values();
+ }
+ }
+}
diff --git a/contrib/grpc_util/README.md b/contrib/grpc_util/README.md
new file mode 100644
index 00000000..7c5c7b99
--- /dev/null
+++ b/contrib/grpc_util/README.md
@@ -0,0 +1,35 @@
+# OpenCensus gRPC Util
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus gRPC Util for Java* is a collection of utilities for trace instrumentation when
+working with [gRPC][grpc-url].
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-grpc-util</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-grpc-util:0.16.1'
+```
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-grpc-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-grpc-util
+[grpc-url]: https://github.com/grpc/grpc-java
diff --git a/contrib/grpc_util/build.gradle b/contrib/grpc_util/build.gradle
new file mode 100644
index 00000000..ecc347d3
--- /dev/null
+++ b/contrib/grpc_util/build.gradle
@@ -0,0 +1,26 @@
+description = 'OpenCensus gRPC Util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api')
+
+ compile (libraries.grpc_core) {
+ // Prefer library version.
+ exclude group: 'com.google.errorprone', module: 'error_prone_annotations'
+
+ // Prefer library version.
+ exclude group: 'com.google.code.findbugs', module: 'jsr305'
+
+ // We will always be more up to date.
+ exclude group: 'io.opencensus', module: 'opencensus-api'
+ }
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/grpc_util/src/main/java/io/opencensus/contrib/grpc/util/StatusConverter.java b/contrib/grpc_util/src/main/java/io/opencensus/contrib/grpc/util/StatusConverter.java
new file mode 100644
index 00000000..92b36d44
--- /dev/null
+++ b/contrib/grpc_util/src/main/java/io/opencensus/contrib/grpc/util/StatusConverter.java
@@ -0,0 +1,165 @@
+/*
+ * 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.contrib.grpc.util;
+
+/**
+ * Utility class to convert between {@link io.opencensus.trace.Status} and {@link io.grpc.Status}.
+ *
+ * @since 0.6
+ */
+public final class StatusConverter {
+
+ /**
+ * Returns a {@link io.opencensus.trace.Status.CanonicalCode} from a {@link io.grpc.Status.Code}.
+ *
+ * @param grpcCode the given {@code io.grpc.Status.Code}.
+ * @return a {@code io.opencensus.trace.Status.CanonicalCode} from a {@code io.grpc.Status.Code}.
+ * @since 0.6
+ */
+ public static io.opencensus.trace.Status.CanonicalCode fromGrpcCode(
+ io.grpc.Status.Code grpcCode) {
+ return opencensusStatusFromGrpcCode(grpcCode).getCanonicalCode();
+ }
+
+ /**
+ * Returns a {@link io.opencensus.trace.Status} from a {@link io.grpc.Status}.
+ *
+ * @param grpcStatus the given {@code io.grpc.Status}.
+ * @return a {@code io.opencensus.trace.Status} from a {@code io.grpc.Status}.
+ * @since 0.6
+ */
+ public static io.opencensus.trace.Status fromGrpcStatus(io.grpc.Status grpcStatus) {
+ io.opencensus.trace.Status status = opencensusStatusFromGrpcCode(grpcStatus.getCode());
+ String description = grpcStatus.getDescription();
+ if (description != null) {
+ status = status.withDescription(description);
+ }
+ return status;
+ }
+
+ /**
+ * Returns a {@link io.grpc.Status.Code} from a {@link io.opencensus.trace.Status.CanonicalCode}.
+ *
+ * @param opencensusCanonicalCode the given {@code io.opencensus.trace.Status.CanonicalCode}.
+ * @return a {@code io.grpc.Status.Code} from a {@code io.opencensus.trace.Status.CanonicalCode}.
+ * @since 0.6
+ */
+ public static io.grpc.Status.Code toGrpcCode(
+ io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode) {
+ return grpcStatusFromOpencensusCanonicalCode(opencensusCanonicalCode).getCode();
+ }
+
+ /**
+ * Returns a {@link io.grpc.Status} from a {@link io.opencensus.trace.Status}.
+ *
+ * @param opencensusStatus the given {@code io.opencensus.trace.Status}.
+ * @return a {@code io.grpc.Status} from a {@code io.opencensus.trace.Status}.
+ * @since 0.6
+ */
+ public static io.grpc.Status toGrpcStatus(io.opencensus.trace.Status opencensusStatus) {
+ io.grpc.Status status =
+ grpcStatusFromOpencensusCanonicalCode(opencensusStatus.getCanonicalCode());
+ if (opencensusStatus.getDescription() != null) {
+ status = status.withDescription(opencensusStatus.getDescription());
+ }
+ return status;
+ }
+
+ private static io.opencensus.trace.Status opencensusStatusFromGrpcCode(
+ io.grpc.Status.Code grpcCanonicaleCode) {
+ switch (grpcCanonicaleCode) {
+ case OK:
+ return io.opencensus.trace.Status.OK;
+ case CANCELLED:
+ return io.opencensus.trace.Status.CANCELLED;
+ case UNKNOWN:
+ return io.opencensus.trace.Status.UNKNOWN;
+ case INVALID_ARGUMENT:
+ return io.opencensus.trace.Status.INVALID_ARGUMENT;
+ case DEADLINE_EXCEEDED:
+ return io.opencensus.trace.Status.DEADLINE_EXCEEDED;
+ case NOT_FOUND:
+ return io.opencensus.trace.Status.NOT_FOUND;
+ case ALREADY_EXISTS:
+ return io.opencensus.trace.Status.ALREADY_EXISTS;
+ case PERMISSION_DENIED:
+ return io.opencensus.trace.Status.PERMISSION_DENIED;
+ case RESOURCE_EXHAUSTED:
+ return io.opencensus.trace.Status.RESOURCE_EXHAUSTED;
+ case FAILED_PRECONDITION:
+ return io.opencensus.trace.Status.FAILED_PRECONDITION;
+ case ABORTED:
+ return io.opencensus.trace.Status.ABORTED;
+ case OUT_OF_RANGE:
+ return io.opencensus.trace.Status.OUT_OF_RANGE;
+ case UNIMPLEMENTED:
+ return io.opencensus.trace.Status.UNIMPLEMENTED;
+ case INTERNAL:
+ return io.opencensus.trace.Status.INTERNAL;
+ case UNAVAILABLE:
+ return io.opencensus.trace.Status.UNAVAILABLE;
+ case DATA_LOSS:
+ return io.opencensus.trace.Status.DATA_LOSS;
+ case UNAUTHENTICATED:
+ return io.opencensus.trace.Status.UNAUTHENTICATED;
+ }
+ throw new AssertionError("Unhandled status code " + grpcCanonicaleCode);
+ }
+
+ private static io.grpc.Status grpcStatusFromOpencensusCanonicalCode(
+ io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode) {
+ switch (opencensusCanonicalCode) {
+ case OK:
+ return io.grpc.Status.OK;
+ case CANCELLED:
+ return io.grpc.Status.CANCELLED;
+ case UNKNOWN:
+ return io.grpc.Status.UNKNOWN;
+ case INVALID_ARGUMENT:
+ return io.grpc.Status.INVALID_ARGUMENT;
+ case DEADLINE_EXCEEDED:
+ return io.grpc.Status.DEADLINE_EXCEEDED;
+ case NOT_FOUND:
+ return io.grpc.Status.NOT_FOUND;
+ case ALREADY_EXISTS:
+ return io.grpc.Status.ALREADY_EXISTS;
+ case PERMISSION_DENIED:
+ return io.grpc.Status.PERMISSION_DENIED;
+ case RESOURCE_EXHAUSTED:
+ return io.grpc.Status.RESOURCE_EXHAUSTED;
+ case FAILED_PRECONDITION:
+ return io.grpc.Status.FAILED_PRECONDITION;
+ case ABORTED:
+ return io.grpc.Status.ABORTED;
+ case OUT_OF_RANGE:
+ return io.grpc.Status.OUT_OF_RANGE;
+ case UNIMPLEMENTED:
+ return io.grpc.Status.UNIMPLEMENTED;
+ case INTERNAL:
+ return io.grpc.Status.INTERNAL;
+ case UNAVAILABLE:
+ return io.grpc.Status.UNAVAILABLE;
+ case DATA_LOSS:
+ return io.grpc.Status.DATA_LOSS;
+ case UNAUTHENTICATED:
+ return io.grpc.Status.UNAUTHENTICATED;
+ }
+ throw new AssertionError("Unhandled status code " + opencensusCanonicalCode);
+ }
+
+ private StatusConverter() {}
+}
diff --git a/contrib/grpc_util/src/test/java/io/opencensus/contrib/grpc/util/StatusConverterTest.java b/contrib/grpc_util/src/test/java/io/opencensus/contrib/grpc/util/StatusConverterTest.java
new file mode 100644
index 00000000..a6b5e87c
--- /dev/null
+++ b/contrib/grpc_util/src/test/java/io/opencensus/contrib/grpc/util/StatusConverterTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.contrib.grpc.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StatusConverter}. */
+@RunWith(JUnit4.class)
+public class StatusConverterTest {
+
+ @Test
+ public void convertFromGrpcCode() {
+ for (io.grpc.Status.Code grpcCanonicalCode : io.grpc.Status.Code.values()) {
+ io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode =
+ StatusConverter.fromGrpcCode(grpcCanonicalCode);
+ assertThat(opencensusCanonicalCode.toString()).isEqualTo(grpcCanonicalCode.toString());
+ }
+ }
+
+ @Test
+ public void convertFromGrpcStatus() {
+ // Without description
+ for (io.grpc.Status.Code grpcCanonicalCode : io.grpc.Status.Code.values()) {
+ io.grpc.Status grpcStatus = io.grpc.Status.fromCode(grpcCanonicalCode);
+ io.opencensus.trace.Status opencensusStatus = StatusConverter.fromGrpcStatus(grpcStatus);
+ assertThat(opencensusStatus.getCanonicalCode().toString())
+ .isEqualTo(grpcStatus.getCode().toString());
+ assertThat(opencensusStatus.getDescription()).isNull();
+ }
+
+ // With description
+ for (io.grpc.Status.Code grpcCanonicalCode : io.grpc.Status.Code.values()) {
+ io.grpc.Status grpcStatus =
+ io.grpc.Status.fromCode(grpcCanonicalCode).withDescription("This is my description");
+ io.opencensus.trace.Status opencensusStatus = StatusConverter.fromGrpcStatus(grpcStatus);
+ assertThat(opencensusStatus.getCanonicalCode().toString())
+ .isEqualTo(grpcStatus.getCode().toString());
+ assertThat(opencensusStatus.getDescription()).isEqualTo(grpcStatus.getDescription());
+ }
+ }
+
+ @Test
+ public void convertToGrpcCode() {
+ for (io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode :
+ io.opencensus.trace.Status.CanonicalCode.values()) {
+ io.grpc.Status.Code grpcCanonicalCode = StatusConverter.toGrpcCode(opencensusCanonicalCode);
+ assertThat(grpcCanonicalCode.toString()).isEqualTo(opencensusCanonicalCode.toString());
+ }
+ }
+
+ @Test
+ public void convertToGrpcStatus() {
+ // Without description
+ for (io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode :
+ io.opencensus.trace.Status.CanonicalCode.values()) {
+ io.opencensus.trace.Status opencensusStatus = opencensusCanonicalCode.toStatus();
+ io.grpc.Status grpcStatus = StatusConverter.toGrpcStatus(opencensusStatus);
+ assertThat(grpcStatus.getCode().toString())
+ .isEqualTo(opencensusStatus.getCanonicalCode().toString());
+ assertThat(grpcStatus.getDescription()).isNull();
+ }
+
+ // With description
+ for (io.opencensus.trace.Status.CanonicalCode opencensusCanonicalCode :
+ io.opencensus.trace.Status.CanonicalCode.values()) {
+ io.opencensus.trace.Status opencensusStatus =
+ opencensusCanonicalCode.toStatus().withDescription("This is my description");
+ io.grpc.Status grpcStatus = StatusConverter.toGrpcStatus(opencensusStatus);
+ assertThat(grpcStatus.getCode().toString())
+ .isEqualTo(opencensusStatus.getCanonicalCode().toString());
+ assertThat(grpcStatus.getDescription()).isEqualTo(opencensusStatus.getDescription());
+ }
+ }
+}
diff --git a/contrib/http_util/README.md b/contrib/http_util/README.md
new file mode 100644
index 00000000..9678fcb7
--- /dev/null
+++ b/contrib/http_util/README.md
@@ -0,0 +1,41 @@
+# OpenCensus HTTP Util
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus HTTP Util for Java* is a collection of utilities for trace instrumentation when
+working with HTTP.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-http-util</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-contrib-http-util:0.16.1'
+```
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-grpc-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-grpc-util
+[grpc-url]: https://github.com/grpc/grpc-java
diff --git a/contrib/http_util/build.gradle b/contrib/http_util/build.gradle
new file mode 100644
index 00000000..a3c9f260
--- /dev/null
+++ b/contrib/http_util/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus HTTP Util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/CloudTraceFormat.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/CloudTraceFormat.java
new file mode 100644
index 00000000..77faa9f9
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/CloudTraceFormat.java
@@ -0,0 +1,149 @@
+/*
+ * 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.contrib.http.util;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.primitives.UnsignedInts;
+import com.google.common.primitives.UnsignedLongs;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.List;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.NonNull;
+*/
+
+/**
+ * Implementation of the "X-Cloud-Trace-Context" format, defined by the Google Cloud Trace.
+ *
+ * <p>The supported format is the following:
+ *
+ * <pre>
+ * &lt;TRACE_ID&gt;/&lt;SPAN_ID&gt;[;o=&lt;TRACE_OPTIONS&gt;]
+ * </pre>
+ *
+ * <ul>
+ * <li>TRACE_ID is a 32-character hex value;
+ * <li>SPAN_ID is a decimal representation of a 64-bit unsigned long;
+ * <li>TRACE_OPTIONS is a decimal representation of a 32-bit unsigned integer. The least
+ * significant bit defines whether a request is traced (1 - enabled, 0 - disabled). Behaviors
+ * of other bits are currently undefined. This value is optional. Although upstream service
+ * may leave this value unset to leave the sampling decision up to downstream client, this
+ * utility will always default it to 0 if absent.
+ * </ul>
+ *
+ * <p>Valid values:
+ *
+ * <ul>
+ * <li>"105445aa7843bc8bf206b120001000/123;o=1"
+ * <li>"105445aa7843bc8bf206b120001000/123"
+ * <li>"105445aa7843bc8bf206b120001000/123;o=0"
+ * </ul>
+ */
+final class CloudTraceFormat extends TextFormat {
+ static final String HEADER_NAME = "X-Cloud-Trace-Context";
+ static final List<String> FIELDS = Collections.singletonList(HEADER_NAME);
+ static final char SPAN_ID_DELIMITER = '/';
+ static final String TRACE_OPTION_DELIMITER = ";o=";
+ static final String SAMPLED = "1";
+ static final String NOT_SAMPLED = "0";
+ static final TraceOptions OPTIONS_SAMPLED = TraceOptions.builder().setIsSampled(true).build();
+ static final TraceOptions OPTIONS_NOT_SAMPLED = TraceOptions.DEFAULT;
+ static final int TRACE_ID_SIZE = 2 * TraceId.SIZE;
+ static final int TRACE_OPTION_DELIMITER_SIZE = TRACE_OPTION_DELIMITER.length();
+ static final int SPAN_ID_START_POS = TRACE_ID_SIZE + 1;
+ // 32-digit TRACE_ID + 1 digit SPAN_ID_DELIMITER + at least 1 digit SPAN_ID
+ static final int MIN_HEADER_SIZE = SPAN_ID_START_POS + 1;
+ static final int CLOUD_TRACE_IS_SAMPLED = 0x1;
+ private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+
+ @Override
+ public List<String> fields() {
+ return FIELDS;
+ }
+
+ @Override
+ public <C /*>>> extends @NonNull Object*/> void inject(
+ SpanContext spanContext, C carrier, Setter<C> setter) {
+ checkNotNull(spanContext, "spanContext");
+ checkNotNull(setter, "setter");
+ checkNotNull(carrier, "carrier");
+ StringBuilder builder =
+ new StringBuilder()
+ .append(spanContext.getTraceId().toLowerBase16())
+ .append(SPAN_ID_DELIMITER)
+ .append(UnsignedLongs.toString(spanIdToLong(spanContext.getSpanId())))
+ .append(TRACE_OPTION_DELIMITER)
+ .append(spanContext.getTraceOptions().isSampled() ? SAMPLED : NOT_SAMPLED);
+
+ setter.put(carrier, HEADER_NAME, builder.toString());
+ }
+
+ @Override
+ public <C /*>>> extends @NonNull Object*/> SpanContext extract(C carrier, Getter<C> getter)
+ throws SpanContextParseException {
+ checkNotNull(carrier, "carrier");
+ checkNotNull(getter, "getter");
+ try {
+ String headerStr = getter.get(carrier, HEADER_NAME);
+ if (headerStr == null || headerStr.length() < MIN_HEADER_SIZE) {
+ throw new SpanContextParseException("Missing or too short header: " + HEADER_NAME);
+ }
+ checkArgument(headerStr.charAt(TRACE_ID_SIZE) == SPAN_ID_DELIMITER, "Invalid TRACE_ID size");
+
+ TraceId traceId = TraceId.fromLowerBase16(headerStr.subSequence(0, TRACE_ID_SIZE));
+ int traceOptionsPos = headerStr.indexOf(TRACE_OPTION_DELIMITER, TRACE_ID_SIZE);
+ CharSequence spanIdStr =
+ headerStr.subSequence(
+ SPAN_ID_START_POS, traceOptionsPos < 0 ? headerStr.length() : traceOptionsPos);
+ SpanId spanId = longToSpanId(UnsignedLongs.parseUnsignedLong(spanIdStr.toString(), 10));
+ TraceOptions traceOptions = OPTIONS_NOT_SAMPLED;
+ if (traceOptionsPos > 0) {
+ String traceOptionsStr = headerStr.substring(traceOptionsPos + TRACE_OPTION_DELIMITER_SIZE);
+ if ((UnsignedInts.parseUnsignedInt(traceOptionsStr, 10) & CLOUD_TRACE_IS_SAMPLED) != 0) {
+ traceOptions = OPTIONS_SAMPLED;
+ }
+ }
+ return SpanContext.create(traceId, spanId, traceOptions, TRACESTATE_DEFAULT);
+ } catch (IllegalArgumentException e) {
+ throw new SpanContextParseException("Invalid input", e);
+ }
+ }
+
+ // Using big-endian encoding.
+ private static SpanId longToSpanId(long x) {
+ ByteBuffer buffer = ByteBuffer.allocate(SpanId.SIZE);
+ buffer.putLong(x);
+ return SpanId.fromBytes(buffer.array());
+ }
+
+ // Using big-endian encoding.
+ private static long spanIdToLong(SpanId spanId) {
+ ByteBuffer buffer = ByteBuffer.allocate(SpanId.SIZE);
+ buffer.put(spanId.getBytes());
+ return buffer.getLong(0);
+ }
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpMeasureConstants.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpMeasureConstants.java
new file mode 100644
index 00000000..fd73b8a9
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpMeasureConstants.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2018, 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.contrib.http.util;
+
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagKey;
+
+/**
+ * A helper class which holds OpenCensus's default HTTP {@link Measure}s and {@link TagKey}s.
+ *
+ * <p>{@link Measure}s and {@link TagKey}s in this class are all public for other
+ * libraries/frameworks to reference and use.
+ *
+ * @since 0.13
+ */
+public final class HttpMeasureConstants {
+
+ private HttpMeasureConstants() {}
+
+ private static final String UNIT_COUNT = "1";
+ private static final String UNIT_SIZE_BYTE = "By";
+ private static final String UNIT_LATENCY_MS = "ms";
+
+ /**
+ * {@link Measure} for the client-side total bytes sent in request body (not including headers).
+ * This is uncompressed bytes.
+ *
+ * @since 0.13
+ */
+ public static final MeasureLong HTTP_CLIENT_SENT_BYTES =
+ Measure.MeasureLong.create(
+ "opencensus.io/http/client/sent_bytes",
+ "Client-side total bytes sent in request body (uncompressed)",
+ UNIT_SIZE_BYTE);
+
+ /**
+ * {@link Measure} for the client-side total bytes received in response bodies (not including
+ * headers but including error responses with bodies). Should be measured from actual bytes
+ * received and read, not the value of the Content-Length header. This is uncompressed bytes.
+ * Responses with no body should record 0 for this value.
+ *
+ * @since 0.13
+ */
+ public static final MeasureLong HTTP_CLIENT_RECEIVED_BYTES =
+ Measure.MeasureLong.create(
+ "opencensus.io/http/client/received_bytes",
+ "Client-side total bytes received in response bodies (uncompressed)",
+ UNIT_SIZE_BYTE);
+
+ /**
+ * {@link Measure} for the client-side time between first byte of request headers sent to last
+ * byte of response received, or terminal error.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble HTTP_CLIENT_ROUNDTRIP_LATENCY =
+ Measure.MeasureDouble.create(
+ "opencensus.io/http/client/roundtrip_latency",
+ "Client-side time between first byte of request headers sent to last byte of response "
+ + "received, or terminal error",
+ UNIT_LATENCY_MS);
+
+ /**
+ * {@link Measure} for the server-side total bytes received in request body (not including
+ * headers). This is uncompressed bytes.
+ *
+ * @since 0.13
+ */
+ public static final MeasureLong HTTP_SERVER_RECEIVED_BYTES =
+ Measure.MeasureLong.create(
+ "opencensus.io/http/server/received_bytes",
+ "Server-side total bytes received in request body (uncompressed)",
+ UNIT_SIZE_BYTE);
+
+ /**
+ * {@link Measure} for the server-side total bytes sent in response bodies (not including headers
+ * but including error responses with bodies). Should be measured from actual bytes written and
+ * sent, not the value of the Content-Length header. This is uncompressed bytes. Responses with no
+ * body should record 0 for this value.
+ *
+ * @since 0.13
+ */
+ public static final MeasureLong HTTP_SERVER_SENT_BYTES =
+ Measure.MeasureLong.create(
+ "opencensus.io/http/server/sent_bytes",
+ "Server-side total bytes sent in response bodies (uncompressed)",
+ UNIT_SIZE_BYTE);
+
+ /**
+ * {@link Measure} for the server-side time between first byte of request headers received to last
+ * byte of response sent, or terminal error.
+ *
+ * @since 0.13
+ */
+ public static final MeasureDouble HTTP_SERVER_LATENCY =
+ Measure.MeasureDouble.create(
+ "opencensus.io/http/server/server_latency",
+ "Server-side time between first byte of request headers received to last byte of "
+ + "response sent, or terminal error",
+ UNIT_LATENCY_MS);
+
+ /**
+ * {@link TagKey} for the value of the client-side HTTP host header.
+ *
+ * @since 0.13
+ */
+ public static final TagKey HTTP_CLIENT_HOST = TagKey.create("http_client_host");
+
+ /**
+ * {@link TagKey} for the value of the server-side HTTP host header.
+ *
+ * @since 0.13
+ */
+ public static final TagKey HTTP_SERVER_HOST = TagKey.create("http_server_host");
+
+ /**
+ * {@link TagKey} for the numeric client-side HTTP response status code (e.g. 200, 404, 500). If a
+ * transport error occurred and no status code was read, use "error" as the {@code TagValue}.
+ *
+ * @since 0.13
+ */
+ public static final TagKey HTTP_CLIENT_STATUS = TagKey.create("http_client_status");
+
+ /**
+ * {@link TagKey} for the numeric server-side HTTP response status code (e.g. 200, 404, 500). If a
+ * transport error occurred and no status code was written, use "error" as the {@code TagValue}.
+ *
+ * @since 0.13
+ */
+ public static final TagKey HTTP_SERVER_STATUS = TagKey.create("http_server_status");
+
+ /**
+ * {@link TagKey} for the client-side URL path (not including query string) in the request.
+ *
+ * @since 0.13
+ */
+ public static final TagKey HTTP_CLIENT_PATH = TagKey.create("http_client_path");
+
+ /**
+ * {@link TagKey} for the server-side URL path (not including query string) in the request.
+ *
+ * @since 0.13
+ */
+ public static final TagKey HTTP_SERVER_PATH = TagKey.create("http_server_path");
+
+ /**
+ * {@link TagKey} for the client-side HTTP method of the request, capitalized (GET, POST, etc.).
+ *
+ * @since 0.13
+ */
+ public static final TagKey HTTP_CLIENT_METHOD = TagKey.create("http_client_method");
+
+ /**
+ * {@link TagKey} for the server-side HTTP method of the request, capitalized (GET, POST, etc.).
+ *
+ * @since 0.13
+ */
+ public static final TagKey HTTP_SERVER_METHOD = TagKey.create("http_server_method");
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpPropagationUtil.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpPropagationUtil.java
new file mode 100644
index 00000000..779be8d8
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpPropagationUtil.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.contrib.http.util;
+
+import io.opencensus.trace.propagation.TextFormat;
+
+/**
+ * Utility class to get all supported {@link TextFormat}.
+ *
+ * @since 0.11.0
+ */
+public class HttpPropagationUtil {
+
+ private HttpPropagationUtil() {}
+
+ /**
+ * Returns the Stack Driver format implementation. The header specification for this format is
+ * "X-Cloud-Trace-Context: &lt;TRACE_ID&gt;/&lt;SPAN_ID&gt;[;o=&lt;TRACE_TRUE&gt;]". See this <a
+ * href="https://cloud.google.com/trace/docs/support">page</a> for more information.
+ *
+ * @since 0.11.0
+ * @return the Stack Driver format.
+ */
+ public static TextFormat getCloudTraceFormat() {
+ return new CloudTraceFormat();
+ }
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViewConstants.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViewConstants.java
new file mode 100644
index 00000000..54ad20ce
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViewConstants.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2018, 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.contrib.http.util;
+
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_METHOD;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_PATH;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_RECEIVED_BYTES;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_SENT_BYTES;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_CLIENT_STATUS;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_LATENCY;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_METHOD;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_PATH;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_RECEIVED_BYTES;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_SENT_BYTES;
+import static io.opencensus.contrib.http.util.HttpMeasureConstants.HTTP_SERVER_STATUS;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.View;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * A helper class that holds OpenCensus's default HTTP {@link View}s.
+ *
+ * <p>{@link View}s in this class are all public for other libraries/frameworks to reference and
+ * use.
+ *
+ * @since 0.13
+ */
+public final class HttpViewConstants {
+
+ private HttpViewConstants() {}
+
+ @VisibleForTesting static final Aggregation COUNT = Count.create();
+
+ @VisibleForTesting
+ static final Aggregation SIZE_DISTRIBUTION =
+ Distribution.create(
+ BucketBoundaries.create(
+ Collections.<Double>unmodifiableList(
+ Arrays.<Double>asList(
+ 0.0,
+ 1024.0,
+ 2048.0,
+ 4096.0,
+ 16384.0,
+ 65536.0,
+ 262144.0,
+ 1048576.0,
+ 4194304.0,
+ 16777216.0,
+ 67108864.0,
+ 268435456.0,
+ 1073741824.0,
+ 4294967296.0))));
+
+ @VisibleForTesting
+ static final Aggregation LATENCY_DISTRIBUTION =
+ Distribution.create(
+ BucketBoundaries.create(
+ Collections.<Double>unmodifiableList(
+ Arrays.<Double>asList(
+ 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0, 25.0, 30.0,
+ 40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0, 300.0, 400.0,
+ 500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
+ 100000.0))));
+
+ /**
+ * {@link View} for count of client-side HTTP requests completed.
+ *
+ * @since 0.13
+ */
+ public static final View HTTP_CLIENT_COMPLETED_COUNT_VIEW =
+ View.create(
+ View.Name.create("opencensus.io/http/client/completed_count"),
+ "Count of client-side HTTP requests completed",
+ HTTP_CLIENT_ROUNDTRIP_LATENCY,
+ COUNT,
+ Arrays.asList(HTTP_CLIENT_METHOD, HTTP_CLIENT_PATH));
+
+ /**
+ * {@link View} for size distribution of client-side HTTP request body.
+ *
+ * @since 0.13
+ */
+ public static final View HTTP_CLIENT_SENT_BYTES_VIEW =
+ View.create(
+ View.Name.create("opencensus.io/http/client/sent_bytes"),
+ "Size distribution of client-side HTTP request body",
+ HTTP_CLIENT_SENT_BYTES,
+ SIZE_DISTRIBUTION,
+ Arrays.asList(HTTP_CLIENT_METHOD, HTTP_CLIENT_PATH));
+
+ /**
+ * {@link View} for size distribution of client-side HTTP response body.
+ *
+ * @since 0.13
+ */
+ public static final View HTTP_CLIENT_RECEIVED_BYTES_VIEW =
+ View.create(
+ View.Name.create("opencensus.io/http/client/received_bytes"),
+ "Size distribution of client-side HTTP response body",
+ HTTP_CLIENT_RECEIVED_BYTES,
+ SIZE_DISTRIBUTION,
+ Arrays.asList(HTTP_CLIENT_METHOD, HTTP_CLIENT_PATH));
+
+ /**
+ * {@link View} for roundtrip latency distribution of client-side HTTP requests.
+ *
+ * @since 0.13
+ */
+ public static final View HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW =
+ View.create(
+ View.Name.create("opencensus.io/http/client/roundtrip_latency"),
+ "Roundtrip latency distribution of client-side HTTP requests",
+ HTTP_CLIENT_ROUNDTRIP_LATENCY,
+ LATENCY_DISTRIBUTION,
+ Arrays.asList(HTTP_CLIENT_METHOD, HTTP_CLIENT_PATH, HTTP_CLIENT_STATUS));
+
+ /**
+ * {@link View} for count of server-side HTTP requests serving completed.
+ *
+ * @since 0.13
+ */
+ public static final View HTTP_SERVER_COMPLETED_COUNT_VIEW =
+ View.create(
+ View.Name.create("opencensus.io/http/server/completed_count"),
+ "Count of HTTP server-side requests serving completed",
+ HTTP_SERVER_LATENCY,
+ COUNT,
+ Arrays.asList(HTTP_SERVER_METHOD, HTTP_SERVER_PATH));
+
+ /**
+ * {@link View} for size distribution of server-side HTTP request body.
+ *
+ * @since 0.13
+ */
+ public static final View HTTP_SERVER_RECEIVED_BYTES_VIEW =
+ View.create(
+ View.Name.create("opencensus.io/http/server/received_bytes"),
+ "Size distribution of server-side HTTP request body",
+ HTTP_SERVER_RECEIVED_BYTES,
+ SIZE_DISTRIBUTION,
+ Arrays.asList(HTTP_SERVER_METHOD, HTTP_SERVER_PATH));
+
+ /**
+ * {@link View} for size distribution of server-side HTTP response body.
+ *
+ * @since 0.13
+ */
+ public static final View HTTP_SERVER_SENT_BYTES_VIEW =
+ View.create(
+ View.Name.create("opencensus.io/http/server/sent_bytes"),
+ "Size distribution of server-side HTTP response body",
+ HTTP_SERVER_SENT_BYTES,
+ SIZE_DISTRIBUTION,
+ Arrays.asList(HTTP_SERVER_METHOD, HTTP_SERVER_PATH));
+
+ /**
+ * {@link View} for latency distribution of server-side HTTP requests serving.
+ *
+ * @since 0.13
+ */
+ public static final View HTTP_SERVER_LATENCY_VIEW =
+ View.create(
+ View.Name.create("opencensus.io/http/server/server_latency"),
+ "Latency distribution of server-side HTTP requests serving",
+ HTTP_SERVER_LATENCY,
+ LATENCY_DISTRIBUTION,
+ Arrays.asList(HTTP_SERVER_METHOD, HTTP_SERVER_PATH, HTTP_SERVER_STATUS));
+}
diff --git a/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViews.java b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViews.java
new file mode 100644
index 00000000..9e3b984e
--- /dev/null
+++ b/contrib/http_util/src/main/java/io/opencensus/contrib/http/util/HttpViews.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018, 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.contrib.http.util;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewManager;
+
+/**
+ * A helper class that allows users to register HTTP views easily.
+ *
+ * @since 0.13
+ */
+public final class HttpViews {
+
+ private HttpViews() {}
+
+ @VisibleForTesting
+ static final ImmutableSet<View> HTTP_SERVER_VIEWS_SET =
+ ImmutableSet.of(
+ HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW,
+ HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW,
+ HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW,
+ HttpViewConstants.HTTP_SERVER_LATENCY_VIEW);
+
+ @VisibleForTesting
+ static final ImmutableSet<View> HTTP_CLIENT_VIEWS_SET =
+ ImmutableSet.of(
+ HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW,
+ HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW,
+ HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW,
+ HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW);
+
+ /**
+ * Register all default client views.
+ *
+ * <p>It is recommended to call this method before doing any HTTP call to avoid missing stats.
+ *
+ * @since 0.13
+ */
+ public static final void registerAllClientViews() {
+ registerAllClientViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerAllClientViews(ViewManager viewManager) {
+ for (View view : HTTP_CLIENT_VIEWS_SET) {
+ viewManager.registerView(view);
+ }
+ }
+
+ /**
+ * Register all default server views.
+ *
+ * <p>It is recommended to call this method before doing any HTTP call to avoid missing stats.
+ *
+ * @since 0.13
+ */
+ public static final void registerAllServerViews() {
+ registerAllServerViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerAllServerViews(ViewManager viewManager) {
+ for (View view : HTTP_SERVER_VIEWS_SET) {
+ viewManager.registerView(view);
+ }
+ }
+
+ /**
+ * Register all default views. Equivalent with calling {@link #registerAllClientViews()} and
+ * {@link #registerAllServerViews()}.
+ *
+ * <p>It is recommended to call this method before doing any HTTP call to avoid missing stats.
+ *
+ * @since 0.13
+ */
+ public static final void registerAllViews() {
+ registerAllViews(Stats.getViewManager());
+ }
+
+ @VisibleForTesting
+ static void registerAllViews(ViewManager viewManager) {
+ registerAllClientViews(viewManager);
+ registerAllServerViews(viewManager);
+ }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/CloudTraceFormatTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/CloudTraceFormatTest.java
new file mode 100644
index 00000000..4492a402
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/CloudTraceFormatTest.java
@@ -0,0 +1,295 @@
+/*
+ * 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.contrib.http.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.HEADER_NAME;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.NOT_SAMPLED;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.SAMPLED;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.SPAN_ID_DELIMITER;
+import static io.opencensus.contrib.http.util.CloudTraceFormat.TRACE_OPTION_DELIMITER;
+
+import com.google.common.primitives.UnsignedLong;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+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 CloudTraceFormat}. */
+@RunWith(JUnit4.class)
+public final class CloudTraceFormatTest {
+ private final CloudTraceFormat cloudTraceFormat = new CloudTraceFormat();
+
+ private static final String TRACE_ID_BASE16 = "ff000000000000000000000000000041";
+ private static final String TRACE_ID_BASE16_SHORT = "ff00000000000041";
+ private static final String TRACE_ID_BASE16_LONG = "0000" + TRACE_ID_BASE16;
+ private static final String TRACE_ID_BASE16_INVALID = "ff00000000000000000000000abcdefg";
+ private static final String SPAN_ID_BASE16 = "ff00000000000041";
+ private static final String SPAN_ID_BASE10 = UnsignedLong.valueOf(SPAN_ID_BASE16, 16).toString();
+ private static final String SPAN_ID_BASE10_NEGATIVE = "-12345";
+ private static final String SPAN_ID_BASE10_MAX_UNSIGNED_LONG = UnsignedLong.MAX_VALUE.toString();
+ private static final String SPAN_ID_BASE16_MAX_UNSIGNED_LONG =
+ UnsignedLong.MAX_VALUE.toString(16);
+ private static final String SPAN_ID_BASE10_VERY_LONG =
+ SPAN_ID_BASE10_MAX_UNSIGNED_LONG + SPAN_ID_BASE10_MAX_UNSIGNED_LONG;
+ private static final String SPAN_ID_BASE10_INVALID = "0x12345";
+ private static final String OPTIONS_SAMPLED_MORE_BITS = "11"; // 1011
+ private static final String OPTIONS_NOT_SAMPLED_MORE_BITS = "10"; // 1010
+ private static final String OPTIONS_NEGATIVE = "-1";
+ private static final String OPTIONS_INVALID = "0x1";
+
+ private static final TraceId TRACE_ID = TraceId.fromLowerBase16(TRACE_ID_BASE16);
+ private static final SpanId SPAN_ID = SpanId.fromLowerBase16(SPAN_ID_BASE16);
+ private static final SpanId SPAN_ID_MAX =
+ SpanId.fromLowerBase16(SPAN_ID_BASE16_MAX_UNSIGNED_LONG);
+
+ private static final TraceOptions TRACE_OPTIONS_SAMPLED =
+ TraceOptions.builder().setIsSampled(true).build();
+ private static final TraceOptions TRACE_OPTIONS_NOT_SAMPLED = TraceOptions.DEFAULT;
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+ private final Setter<Map<String, String>> setter =
+ new Setter<Map<String, String>>() {
+ @Override
+ public void put(Map<String, String> carrier, String key, String value) {
+ carrier.put(key, value);
+ }
+ };
+ private final Getter<Map<String, String>> getter =
+ new Getter<Map<String, String>>() {
+ @Nullable
+ @Override
+ public String get(Map<String, String> carrier, String key) {
+ return carrier.get(key);
+ }
+ };
+
+ private static String constructHeader(String traceId, String spanId) {
+ return traceId + SPAN_ID_DELIMITER + spanId;
+ }
+
+ private static String constructHeader(String traceId, String spanId, String traceOptions) {
+ return traceId + SPAN_ID_DELIMITER + spanId + TRACE_OPTION_DELIMITER + traceOptions;
+ }
+
+ private void parseSuccess(String headerValue, SpanContext expected)
+ throws SpanContextParseException {
+ Map<String, String> header = new HashMap<String, String>();
+ header.put(HEADER_NAME, headerValue);
+ assertThat(cloudTraceFormat.extract(header, getter)).isEqualTo(expected);
+ }
+
+ private void parseFailure(
+ String headerValue, Class<? extends Throwable> expectedThrown, String expectedMessage)
+ throws SpanContextParseException {
+ Map<String, String> header = new HashMap<String, String>();
+ header.put(HEADER_NAME, headerValue);
+ thrown.expect(expectedThrown);
+ thrown.expectMessage(expectedMessage);
+ cloudTraceFormat.extract(header, getter);
+ }
+
+ @Test
+ public void serializeSampledContextShouldSucceed() throws SpanContextParseException {
+ Map<String, String> carrier = new HashMap<String, String>();
+ cloudTraceFormat.inject(
+ SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_SAMPLED), carrier, setter);
+ assertThat(carrier)
+ .containsExactly(HEADER_NAME, constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, SAMPLED));
+ }
+
+ @Test
+ public void serializeNotSampledContextShouldSucceed() throws SpanContextParseException {
+ Map<String, String> carrier = new HashMap<String, String>();
+ cloudTraceFormat.inject(
+ SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_NOT_SAMPLED), carrier, setter);
+ assertThat(carrier)
+ .containsExactly(
+ HEADER_NAME, constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, NOT_SAMPLED));
+ }
+
+ @Test
+ public void parseSampledShouldSucceed() throws SpanContextParseException {
+ parseSuccess(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, OPTIONS_SAMPLED_MORE_BITS),
+ SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_SAMPLED));
+ }
+
+ @Test
+ public void parseNotSampledShouldSucceed() throws SpanContextParseException {
+ parseSuccess(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, OPTIONS_NOT_SAMPLED_MORE_BITS),
+ SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_NOT_SAMPLED));
+ }
+
+ @Test
+ public void parseMissingTraceOptionsShouldSucceed() throws SpanContextParseException {
+ parseSuccess(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10),
+ SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS_NOT_SAMPLED));
+ }
+
+ @Test
+ public void parseEmptyTraceOptionsShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, ""),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseNegativeTraceOptionsShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, OPTIONS_NEGATIVE),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseInvalidTraceOptionsShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10, OPTIONS_INVALID),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseMissingHeaderShouldFail() throws SpanContextParseException {
+ Map<String, String> headerMissing = new HashMap<String, String>();
+ thrown.expect(SpanContextParseException.class);
+ thrown.expectMessage("Missing or too short header: X-Cloud-Trace-Context");
+ cloudTraceFormat.extract(headerMissing, getter);
+ }
+
+ @Test
+ public void parseEmptyHeaderShouldFail() throws SpanContextParseException {
+ parseFailure(
+ "", SpanContextParseException.class, "Missing or too short header: X-Cloud-Trace-Context");
+ }
+
+ @Test
+ public void parseShortHeaderShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16, ""),
+ SpanContextParseException.class,
+ "Missing or too short header: X-Cloud-Trace-Context");
+ }
+
+ @Test
+ public void parseShortTraceIdShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16_SHORT, SPAN_ID_BASE10, SAMPLED),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseLongTraceIdShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16_LONG, SPAN_ID_BASE10, SAMPLED),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseMissingTraceIdShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader("", SPAN_ID_BASE10_VERY_LONG, SAMPLED),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseInvalidTraceIdShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16_INVALID, SPAN_ID_BASE10, SAMPLED),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseMissingSpanIdDelimiterShouldFail() throws SpanContextParseException {
+ parseFailure(TRACE_ID_BASE16_LONG, SpanContextParseException.class, "Invalid input");
+ }
+
+ @Test
+ public void parseNegativeSpanIdShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10_NEGATIVE, SAMPLED),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseMaxUnsignedLongSpanIdShouldSucceed() throws SpanContextParseException {
+ parseSuccess(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10_MAX_UNSIGNED_LONG, SAMPLED),
+ SpanContext.create(TRACE_ID, SPAN_ID_MAX, TRACE_OPTIONS_SAMPLED));
+ }
+
+ @Test
+ public void parseOverflowSpanIdShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10_VERY_LONG, SAMPLED),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseInvalidSpanIdShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16, SPAN_ID_BASE10_INVALID, SAMPLED),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void parseMissingSpanIdShouldFail() throws SpanContextParseException {
+ parseFailure(
+ constructHeader(TRACE_ID_BASE16, "", SAMPLED),
+ SpanContextParseException.class,
+ "Invalid input");
+ }
+
+ @Test
+ public void fieldsShouldMatch() {
+ assertThat(cloudTraceFormat.fields()).containsExactly(HEADER_NAME);
+ }
+
+ @Test
+ public void parseWithShortSpanIdAndSamplingShouldSucceed() throws SpanContextParseException {
+ final String spanId = "1";
+ ByteBuffer buffer = ByteBuffer.allocate(SpanId.SIZE);
+ buffer.putLong(Long.parseLong(spanId));
+ SpanId expectedSpanId = SpanId.fromBytes(buffer.array());
+ parseSuccess(
+ constructHeader(TRACE_ID_BASE16, spanId, SAMPLED),
+ SpanContext.create(TRACE_ID, expectedSpanId, TRACE_OPTIONS_SAMPLED));
+ }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpMeasureConstantsTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpMeasureConstantsTest.java
new file mode 100644
index 00000000..dd1c20fb
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpMeasureConstantsTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018, 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.contrib.http.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.tags.TagKey;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link HttpMeasureConstants}. */
+@RunWith(JUnit4.class)
+public class HttpMeasureConstantsTest {
+
+ @Test
+ public void constants() {
+ // Test TagKeys
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_STATUS)
+ .isEqualTo(TagKey.create("http_client_status"));
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_METHOD)
+ .isEqualTo(TagKey.create("http_client_method"));
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_PATH).isEqualTo(TagKey.create("http_client_path"));
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_HOST).isEqualTo(TagKey.create("http_client_host"));
+ assertThat(HttpMeasureConstants.HTTP_SERVER_STATUS)
+ .isEqualTo(TagKey.create("http_server_status"));
+ assertThat(HttpMeasureConstants.HTTP_SERVER_METHOD)
+ .isEqualTo(TagKey.create("http_server_method"));
+ assertThat(HttpMeasureConstants.HTTP_SERVER_PATH).isEqualTo(TagKey.create("http_server_path"));
+ assertThat(HttpMeasureConstants.HTTP_SERVER_HOST).isEqualTo(TagKey.create("http_server_host"));
+
+ // Test measures
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_SENT_BYTES.getUnit()).isEqualTo("By");
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_RECEIVED_BYTES.getUnit()).isEqualTo("By");
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY.getUnit()).isEqualTo("ms");
+ assertThat(HttpMeasureConstants.HTTP_SERVER_RECEIVED_BYTES.getUnit()).isEqualTo("By");
+ assertThat(HttpMeasureConstants.HTTP_SERVER_SENT_BYTES.getUnit()).isEqualTo("By");
+ assertThat(HttpMeasureConstants.HTTP_SERVER_LATENCY.getUnit()).isEqualTo("ms");
+
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_SENT_BYTES.getName())
+ .contains("opencensus.io/http/client");
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_RECEIVED_BYTES.getName())
+ .contains("opencensus.io/http/client");
+ assertThat(HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY.getName())
+ .contains("opencensus.io/http/client");
+ assertThat(HttpMeasureConstants.HTTP_SERVER_RECEIVED_BYTES.getName())
+ .contains("opencensus.io/http/server");
+ assertThat(HttpMeasureConstants.HTTP_SERVER_SENT_BYTES.getName())
+ .contains("opencensus.io/http/server");
+ assertThat(HttpMeasureConstants.HTTP_SERVER_LATENCY.getName())
+ .contains("opencensus.io/http/server");
+ }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpPropagationUtilTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpPropagationUtilTest.java
new file mode 100644
index 00000000..f84a4da0
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpPropagationUtilTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018, 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.contrib.http.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.propagation.TextFormat;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link HttpPropagationUtil}. */
+@RunWith(JUnit4.class)
+public class HttpPropagationUtilTest {
+
+ @Test
+ public void cloudTraceFormatNotNull() {
+ TextFormat cloudTraceFormat = HttpPropagationUtil.getCloudTraceFormat();
+ assertThat(cloudTraceFormat).isNotNull();
+ assertThat(cloudTraceFormat).isInstanceOf(CloudTraceFormat.class);
+ }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewConstantsTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewConstantsTest.java
new file mode 100644
index 00000000..d008348e
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewConstantsTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2018, 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.contrib.http.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link HttpViewConstants}. */
+@RunWith(JUnit4.class)
+public class HttpViewConstantsTest {
+
+ @Test
+ public void constants() {
+ // Test aggregations, and their bucket boundaries (if they are Distribution).
+ assertThat(HttpViewConstants.COUNT).isEqualTo(Count.create());
+ assertThat(HttpViewConstants.SIZE_DISTRIBUTION).isInstanceOf(Distribution.class);
+ assertThat(
+ ((Distribution) HttpViewConstants.SIZE_DISTRIBUTION)
+ .getBucketBoundaries()
+ .getBoundaries())
+ .containsExactly(
+ 0.0,
+ 1024.0,
+ 2048.0,
+ 4096.0,
+ 16384.0,
+ 65536.0,
+ 262144.0,
+ 1048576.0,
+ 4194304.0,
+ 16777216.0,
+ 67108864.0,
+ 268435456.0,
+ 1073741824.0,
+ 4294967296.0)
+ .inOrder();
+ assertThat(HttpViewConstants.LATENCY_DISTRIBUTION).isInstanceOf(Distribution.class);
+ assertThat(
+ ((Distribution) HttpViewConstants.LATENCY_DISTRIBUTION)
+ .getBucketBoundaries()
+ .getBoundaries())
+ .containsExactly(
+ 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0, 25.0, 30.0, 40.0, 50.0,
+ 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0, 300.0, 400.0, 500.0, 650.0, 800.0,
+ 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0, 100000.0)
+ .inOrder();
+
+ // Test views.
+ assertThat(HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW.getName().asString())
+ .contains("opencensus.io/http/client");
+ assertThat(HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW.getName().asString())
+ .contains("opencensus.io/http/client");
+ assertThat(HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW.getName().asString())
+ .contains("opencensus.io/http/client");
+ assertThat(HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW.getName().asString())
+ .contains("opencensus.io/http/client");
+ assertThat(HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW.getName().asString())
+ .contains("opencensus.io/http/server");
+ assertThat(HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW.getName().asString())
+ .contains("opencensus.io/http/server");
+ assertThat(HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW.getName().asString())
+ .contains("opencensus.io/http/server");
+ assertThat(HttpViewConstants.HTTP_SERVER_LATENCY_VIEW.getName().asString())
+ .contains("opencensus.io/http/server");
+
+ assertThat(HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW.getMeasure())
+ .isEqualTo(HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY);
+ assertThat(HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW.getMeasure())
+ .isEqualTo(HttpMeasureConstants.HTTP_CLIENT_SENT_BYTES);
+ assertThat(HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW.getMeasure())
+ .isEqualTo(HttpMeasureConstants.HTTP_CLIENT_RECEIVED_BYTES);
+ assertThat(HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW.getMeasure())
+ .isEqualTo(HttpMeasureConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY);
+ assertThat(HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW.getMeasure())
+ .isEqualTo(HttpMeasureConstants.HTTP_SERVER_LATENCY);
+ assertThat(HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW.getMeasure())
+ .isEqualTo(HttpMeasureConstants.HTTP_SERVER_RECEIVED_BYTES);
+ assertThat(HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW.getMeasure())
+ .isEqualTo(HttpMeasureConstants.HTTP_SERVER_SENT_BYTES);
+ assertThat(HttpViewConstants.HTTP_SERVER_LATENCY_VIEW.getMeasure())
+ .isEqualTo(HttpMeasureConstants.HTTP_SERVER_LATENCY);
+
+ assertThat(HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW.getAggregation())
+ .isEqualTo(HttpViewConstants.COUNT);
+ assertThat(HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW.getAggregation())
+ .isEqualTo(HttpViewConstants.SIZE_DISTRIBUTION);
+ assertThat(HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW.getAggregation())
+ .isEqualTo(HttpViewConstants.SIZE_DISTRIBUTION);
+ assertThat(HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW.getAggregation())
+ .isEqualTo(HttpViewConstants.LATENCY_DISTRIBUTION);
+ assertThat(HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW.getAggregation())
+ .isEqualTo(HttpViewConstants.COUNT);
+ assertThat(HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW.getAggregation())
+ .isEqualTo(HttpViewConstants.SIZE_DISTRIBUTION);
+ assertThat(HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW.getAggregation())
+ .isEqualTo(HttpViewConstants.SIZE_DISTRIBUTION);
+ assertThat(HttpViewConstants.HTTP_SERVER_LATENCY_VIEW.getAggregation())
+ .isEqualTo(HttpViewConstants.LATENCY_DISTRIBUTION);
+
+ assertThat(HttpViewConstants.HTTP_CLIENT_COMPLETED_COUNT_VIEW.getColumns())
+ .containsExactly(
+ HttpMeasureConstants.HTTP_CLIENT_METHOD, HttpMeasureConstants.HTTP_CLIENT_PATH);
+ assertThat(HttpViewConstants.HTTP_CLIENT_SENT_BYTES_VIEW.getColumns())
+ .containsExactly(
+ HttpMeasureConstants.HTTP_CLIENT_METHOD, HttpMeasureConstants.HTTP_CLIENT_PATH);
+ assertThat(HttpViewConstants.HTTP_CLIENT_RECEIVED_BYTES_VIEW.getColumns())
+ .containsExactly(
+ HttpMeasureConstants.HTTP_CLIENT_METHOD, HttpMeasureConstants.HTTP_CLIENT_PATH);
+ assertThat(HttpViewConstants.HTTP_CLIENT_ROUNDTRIP_LATENCY_VIEW.getColumns())
+ .containsExactly(
+ HttpMeasureConstants.HTTP_CLIENT_METHOD,
+ HttpMeasureConstants.HTTP_CLIENT_PATH,
+ HttpMeasureConstants.HTTP_CLIENT_STATUS);
+ assertThat(HttpViewConstants.HTTP_SERVER_COMPLETED_COUNT_VIEW.getColumns())
+ .containsExactly(
+ HttpMeasureConstants.HTTP_SERVER_METHOD, HttpMeasureConstants.HTTP_SERVER_PATH);
+ assertThat(HttpViewConstants.HTTP_SERVER_RECEIVED_BYTES_VIEW.getColumns())
+ .containsExactly(
+ HttpMeasureConstants.HTTP_SERVER_METHOD, HttpMeasureConstants.HTTP_SERVER_PATH);
+ assertThat(HttpViewConstants.HTTP_SERVER_SENT_BYTES_VIEW.getColumns())
+ .containsExactly(
+ HttpMeasureConstants.HTTP_SERVER_METHOD, HttpMeasureConstants.HTTP_SERVER_PATH);
+ assertThat(HttpViewConstants.HTTP_SERVER_LATENCY_VIEW.getColumns())
+ .containsExactly(
+ HttpMeasureConstants.HTTP_SERVER_METHOD,
+ HttpMeasureConstants.HTTP_SERVER_PATH,
+ HttpMeasureConstants.HTTP_SERVER_STATUS);
+ }
+}
diff --git a/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewsTest.java b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewsTest.java
new file mode 100644
index 00000000..8adf0a5b
--- /dev/null
+++ b/contrib/http_util/src/test/java/io/opencensus/contrib/http/util/HttpViewsTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018, 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.contrib.http.util;
+
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewManager;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Test for {@link HttpViews}. */
+@RunWith(JUnit4.class)
+public class HttpViewsTest {
+
+ @Mock ViewManager viewManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerClientViews() {
+ HttpViews.registerAllClientViews(viewManager);
+ for (View view : HttpViews.HTTP_CLIENT_VIEWS_SET) {
+ verify(viewManager).registerView(view);
+ }
+ }
+
+ @Test
+ public void registerServerViews() {
+ HttpViews.registerAllServerViews(viewManager);
+ for (View view : HttpViews.HTTP_SERVER_VIEWS_SET) {
+ verify(viewManager).registerView(view);
+ }
+ }
+
+ @Test
+ public void registerAll() {
+ HttpViews.registerAllViews(viewManager);
+ for (View view : HttpViews.HTTP_CLIENT_VIEWS_SET) {
+ verify(viewManager).registerView(view);
+ }
+ for (View view : HttpViews.HTTP_SERVER_VIEWS_SET) {
+ verify(viewManager).registerView(view);
+ }
+ }
+}
diff --git a/contrib/log_correlation/log4j2/README.md b/contrib/log_correlation/log4j2/README.md
new file mode 100644
index 00000000..a5bf1449
--- /dev/null
+++ b/contrib/log_correlation/log4j2/README.md
@@ -0,0 +1,88 @@
+# OpenCensus Log4j 2 Log Correlation
+
+This subproject is currently experimental, so it may be redesigned or removed in the future. It
+will remain experimental until we have a specification for a log correlation feature in
+[opencensus-specs](https://github.com/census-instrumentation/opencensus-specs/)
+(issue [#123](https://github.com/census-instrumentation/opencensus-specs/issues/123)).
+
+The `opencensus-contrib-log-correlation-log4j2` artifact provides a
+[Log4j 2](https://logging.apache.org/log4j/2.x/)
+[`ContextDataInjector`](https://logging.apache.org/log4j/2.x/manual/extending.html#Custom_ContextDataInjector)
+that automatically adds tracing data to the context of Log4j
+[`LogEvent`](https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html)s.
+The class name is
+`OpenCensusTraceContextDataInjector`. `OpenCensusTraceContextDataInjector` adds the current trace
+ID, span ID, and sampling decision to each `LogEvent`, so that they can be accessed with
+[`LogEvent.getContextData()`](https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getContextData())
+or included in a layout.
+
+See
+https://github.com/census-ecosystem/opencensus-experiments/tree/master/java/log_correlation/log4j2
+for a demo that uses this library to correlate logs and traces in Stackdriver.
+
+## Instructions
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-log-correlation-log4j2</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+runtime 'io.opencensus:opencensus-contrib-log-correlation-log4j2:0.16.1'
+```
+
+### Configure the `OpenCensusTraceContextDataInjector`
+
+#### Specify the `ContextDataInjector` override
+
+Override Log4j's default `ContextDataInjector` by setting the system property
+`log4j2.contextDataInjector` to the full name of the class,
+`io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector`.
+
+#### Choose when to add tracing data to log events
+
+The following system property controls the decision to add tracing data from the current span to a
+log event:
+
+`io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.spanSelection`
+
+The allowed values are:
+
+* `ALL_SPANS`: adds tracing data to all log events (default)
+
+* `NO_SPANS`: disables the log correlation feature
+
+* `SAMPLED_SPANS`: adds tracing data to log events when the current span is sampled
+
+### Add the tracing data to log entries
+
+`opencensus-contrib-log-correlation-log4j2` adds the following key-value pairs to the `LogEvent`
+context:
+
+* `opencensusTraceId` - the lowercase base16 encoding of the current trace ID
+* `opencensusSpanId` - the lowercase base16 encoding of the current span ID
+* `opencensusTraceSampled` - the sampling decision of the current span ("true" or "false")
+
+These values can be accessed from layouts with
+[Context Map Lookup](http://logging.apache.org/log4j/2.x/manual/lookups.html#ContextMapLookup). For
+example, the trace ID can be accessed with `$${ctx:opencensusTraceId}`. The values can also be
+accessed with the `X` conversion character in
+[`PatternLayout`](http://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout), for
+example, `%X{opencensusTraceId}`.
+
+See an example Log4j configuration file in the demo:
+https://github.com/census-ecosystem/opencensus-experiments/tree/master/java/log_correlation/log4j2/src/main/resources/log4j2.xml
+
+### Java Versions
+
+Java 6 or above is required for using this artifact.
diff --git a/contrib/log_correlation/log4j2/build.gradle b/contrib/log_correlation/log4j2/build.gradle
new file mode 100644
index 00000000..4a4a6ebc
--- /dev/null
+++ b/contrib/log_correlation/log4j2/build.gradle
@@ -0,0 +1,26 @@
+description = 'OpenCensus Log4j 2 Log Correlation'
+
+apply plugin: 'java'
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.log4j2
+
+ testCompile libraries.guava
+
+ signature "org.codehaus.mojo.signature:java16:+@signature"
+}
+
+compileTestJava {
+ sourceCompatibility = "1.7"
+ targetCompatibility = "1.7"
+}
+
+test {
+ systemProperties['log4j2.contextDataInjector'] =
+ 'io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector'
+
+ // Each test class should run in a separate JVM. See the comment in
+ // AbstractOpenCensusLog4jLogCorrelationTest.
+ forkEvery = 1
+}
diff --git a/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/ContextDataUtils.java b/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/ContextDataUtils.java
new file mode 100644
index 00000000..dd32e448
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/ContextDataUtils.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.log4j2;
+
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.unsafe.ContextUtils;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.spi.ReadOnlyThreadContextMap;
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.apache.logging.log4j.util.TriConsumer;
+
+// Implementation of the methods inherited from ContextDataInjector.
+//
+// This class uses "shareable" to mean that a method's return value can be passed to another
+// thread.
+final class ContextDataUtils {
+ private ContextDataUtils() {}
+
+ // The implementation of this method is based on the example in the Javadocs for
+ // ContextDataInjector.injectContextData.
+ static StringMap injectContextData(
+ SpanSelection spanSelection, @Nullable List<Property> properties, StringMap reusable) {
+ if (properties == null || properties.isEmpty()) {
+ return shareableRawContextData(spanSelection);
+ }
+ // Context data has precedence over configuration properties.
+ putProperties(properties, reusable);
+ // TODO(sebright): The following line can be optimized. See
+ // https://github.com/census-instrumentation/opencensus-java/pull/1422/files#r216425494.
+ reusable.putAll(nonShareableRawContextData(spanSelection));
+ return reusable;
+ }
+
+ private static void putProperties(Collection<Property> properties, StringMap stringMap) {
+ for (Property property : properties) {
+ stringMap.putValue(property.getName(), property.getValue());
+ }
+ }
+
+ private static StringMap shareableRawContextData(SpanSelection spanSelection) {
+ SpanContext spanContext = shouldAddTracingDataToLogEvent(spanSelection);
+ return spanContext == null
+ ? getShareableContextData()
+ : getShareableContextAndTracingData(spanContext);
+ }
+
+ static ReadOnlyStringMap nonShareableRawContextData(SpanSelection spanSelection) {
+ SpanContext spanContext = shouldAddTracingDataToLogEvent(spanSelection);
+ return spanContext == null
+ ? getNonShareableContextData()
+ : getShareableContextAndTracingData(spanContext);
+ }
+
+ // This method returns the current span context iff tracing data should be added to the LogEvent.
+ // It avoids getting the current span when the feature is disabled, for efficiency.
+ @Nullable
+ private static SpanContext shouldAddTracingDataToLogEvent(SpanSelection spanSelection) {
+ switch (spanSelection) {
+ case NO_SPANS:
+ return null;
+ case SAMPLED_SPANS:
+ SpanContext spanContext = getCurrentSpanContext();
+ if (spanContext.getTraceOptions().isSampled()) {
+ return spanContext;
+ } else {
+ return null;
+ }
+ case ALL_SPANS:
+ return getCurrentSpanContext();
+ }
+ throw new AssertionError("Unknown spanSelection: " + spanSelection);
+ }
+
+ private static StringMap getShareableContextData() {
+ ReadOnlyThreadContextMap context = ThreadContext.getThreadContextMap();
+
+ // Return a new object, since StringMap is modifiable.
+ return context == null
+ ? new SortedArrayStringMap(ThreadContext.getImmutableContext())
+ : new SortedArrayStringMap(context.getReadOnlyContextData());
+ }
+
+ private static ReadOnlyStringMap getNonShareableContextData() {
+ ReadOnlyThreadContextMap context = ThreadContext.getThreadContextMap();
+ if (context != null) {
+ return context.getReadOnlyContextData();
+ } else {
+ Map<String, String> contextMap = ThreadContext.getImmutableContext();
+ return contextMap.isEmpty()
+ ? UnmodifiableReadOnlyStringMap.EMPTY
+ : new UnmodifiableReadOnlyStringMap(contextMap);
+ }
+ }
+
+ private static StringMap getShareableContextAndTracingData(SpanContext spanContext) {
+ ReadOnlyThreadContextMap context = ThreadContext.getThreadContextMap();
+ SortedArrayStringMap stringMap;
+ if (context == null) {
+ stringMap = new SortedArrayStringMap(ThreadContext.getImmutableContext());
+ } else {
+ StringMap contextData = context.getReadOnlyContextData();
+ stringMap = new SortedArrayStringMap(contextData.size() + 3);
+ stringMap.putAll(contextData);
+ }
+ // TODO(sebright): Move the calls to TraceId.toLowerBase16() and SpanId.toLowerBase16() out of
+ // the critical path by wrapping the trace and span IDs in objects that call toLowerBase16() in
+ // their toString() methods, after there is a fix for
+ // https://github.com/census-instrumentation/opencensus-java/issues/1436.
+ stringMap.putValue(
+ OpenCensusTraceContextDataInjector.TRACE_ID_CONTEXT_KEY,
+ spanContext.getTraceId().toLowerBase16());
+ stringMap.putValue(
+ OpenCensusTraceContextDataInjector.SPAN_ID_CONTEXT_KEY,
+ spanContext.getSpanId().toLowerBase16());
+ stringMap.putValue(
+ OpenCensusTraceContextDataInjector.TRACE_SAMPLED_CONTEXT_KEY,
+ spanContext.getTraceOptions().isSampled() ? "true" : "false");
+ return stringMap;
+ }
+
+ private static SpanContext getCurrentSpanContext() {
+ Span span = ContextUtils.CONTEXT_SPAN_KEY.get();
+ return span == null ? SpanContext.INVALID : span.getContext();
+ }
+
+ @Immutable
+ private static final class UnmodifiableReadOnlyStringMap implements ReadOnlyStringMap {
+ private static final long serialVersionUID = 0L;
+
+ static final ReadOnlyStringMap EMPTY =
+ new UnmodifiableReadOnlyStringMap(Collections.<String, String>emptyMap());
+
+ private final Map<String, String> map;
+
+ UnmodifiableReadOnlyStringMap(Map<String, String> map) {
+ this.map = map;
+ }
+
+ @Override
+ public boolean containsKey(String key) {
+ return map.containsKey(key);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <V> void forEach(BiConsumer<String, ? super V> action) {
+ for (Entry<String, String> entry : map.entrySet()) {
+ action.accept(entry.getKey(), (V) entry.getValue());
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <V, S> void forEach(TriConsumer<String, ? super V, S> action, S state) {
+ for (Entry<String, String> entry : map.entrySet()) {
+ action.accept(entry.getKey(), (V) entry.getValue(), state);
+ }
+ }
+
+ @Override
+ @Nullable
+ @SuppressWarnings({
+ "unchecked",
+ "TypeParameterUnusedInFormals" // This is an overridden method.
+ })
+ public <V> V getValue(String key) {
+ return (V) map.get(key);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ @Override
+ public int size() {
+ return map.size();
+ }
+
+ @Override
+ public Map<String, String> toMap() {
+ return map;
+ }
+ }
+}
diff --git a/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjector.java b/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjector.java
new file mode 100644
index 00000000..38b18826
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/main/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjector.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.log4j2;
+
+import io.opencensus.common.ExperimentalApi;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.logging.log4j.core.ContextDataInjector;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.StringMap;
+
+/**
+ * A Log4j {@link ContextDataInjector} that adds OpenCensus tracing data to log events.
+ *
+ * <p>This class adds the following key-value pairs:
+ *
+ * <ul>
+ * <li>{@value #TRACE_ID_CONTEXT_KEY} - the lowercase base16 encoding of the current trace ID
+ * <li>{@value #SPAN_ID_CONTEXT_KEY} - the lowercase base16 encoding of the current span ID
+ * <li>{@value #TRACE_SAMPLED_CONTEXT_KEY} - the sampling decision of the current span ({@code
+ * "true"} or {@code "false"})
+ * </ul>
+ *
+ * <p>The tracing data can be accessed with {@link LogEvent#getContextData} or included in a {@link
+ * Layout}. For example, the following patterns could be used to include the tracing data with a <a
+ * href="https://logging.apache.org/log4j/2.x/manual/layouts.html#Pattern_Layout">Pattern
+ * Layout</a>:
+ *
+ * <ul>
+ * <li><code>%X{opencensusTraceId}</code>
+ * <li><code>%X{opencensusSpanId}</code>
+ * <li><code>%X{opencensusTraceSampled}</code>
+ * </ul>
+ *
+ * <p>This feature is currently experimental.
+ *
+ * @since 0.16
+ * @see <a
+ * href="https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/ContextDataInjector.html">org.apache.logging.log4j.core.ContextDataInjector</a>
+ */
+@ExperimentalApi
+public final class OpenCensusTraceContextDataInjector implements ContextDataInjector {
+ private static final SpanSelection DEFAULT_SPAN_SELECTION = SpanSelection.ALL_SPANS;
+
+ /**
+ * Context key for the current trace ID. The name is {@value}.
+ *
+ * @since 0.16
+ */
+ public static final String TRACE_ID_CONTEXT_KEY = "opencensusTraceId";
+
+ /**
+ * Context key for the current span ID. The name is {@value}.
+ *
+ * @since 0.16
+ */
+ public static final String SPAN_ID_CONTEXT_KEY = "opencensusSpanId";
+
+ /**
+ * Context key for the sampling decision of the current span. The name is {@value}.
+ *
+ * @since 0.16
+ */
+ public static final String TRACE_SAMPLED_CONTEXT_KEY = "opencensusTraceSampled";
+
+ /**
+ * Name of the property that defines the {@link SpanSelection}. The name is {@value}.
+ *
+ * @since 0.16
+ */
+ public static final String SPAN_SELECTION_PROPERTY_NAME =
+ "io.opencensus.contrib.logcorrelation.log4j2."
+ + "OpenCensusTraceContextDataInjector.spanSelection";
+
+ private final SpanSelection spanSelection;
+
+ /**
+ * How to decide whether to add tracing data from the current span to a log entry.
+ *
+ * @since 0.16
+ */
+ public enum SpanSelection {
+
+ /**
+ * Never add tracing data to log entries. This constant disables the log correlation feature.
+ *
+ * @since 0.16
+ */
+ NO_SPANS,
+
+ /**
+ * Add tracing data to a log entry iff the current span is sampled.
+ *
+ * @since 0.16
+ */
+ SAMPLED_SPANS,
+
+ /**
+ * Always add tracing data to log entries, even when the current span is not sampled. This is
+ * the default.
+ *
+ * @since 0.16
+ */
+ ALL_SPANS
+ }
+
+ /**
+ * Returns the {@code SpanSelection} setting for this instance.
+ *
+ * @return the {@code SpanSelection} setting for this instance.
+ * @since 0.16
+ */
+ public SpanSelection getSpanSelection() {
+ return spanSelection;
+ }
+
+ /**
+ * Constructor to be called by Log4j.
+ *
+ * <p>This constructor looks up the {@link SpanSelection} using the system property {@link
+ * #SPAN_SELECTION_PROPERTY_NAME}.
+ *
+ * @since 0.16
+ */
+ public OpenCensusTraceContextDataInjector() {
+ this(lookUpSpanSelectionProperty());
+ }
+
+ // visible for testing
+ OpenCensusTraceContextDataInjector(SpanSelection spanSelection) {
+ this.spanSelection = spanSelection;
+ }
+
+ private static SpanSelection lookUpSpanSelectionProperty() {
+ String spanSelectionProperty = System.getProperty(SPAN_SELECTION_PROPERTY_NAME);
+ return spanSelectionProperty == null || spanSelectionProperty.isEmpty()
+ ? DEFAULT_SPAN_SELECTION
+ : parseSpanSelection(spanSelectionProperty);
+ }
+
+ private static SpanSelection parseSpanSelection(String spanSelection) {
+ try {
+ return SpanSelection.valueOf(spanSelection);
+ } catch (IllegalArgumentException e) {
+ return DEFAULT_SPAN_SELECTION;
+ }
+ }
+
+ // Note that this method must return an object that can be passed to another thread.
+ @Override
+ public StringMap injectContextData(@Nullable List<Property> properties, StringMap reusable) {
+ return ContextDataUtils.injectContextData(spanSelection, properties, reusable);
+ }
+
+ // Note that this method does not need to return an object that can be passed to another thread.
+ @Override
+ public ReadOnlyStringMap rawContextData() {
+ return ContextDataUtils.nonShareableRawContextData(spanSelection);
+ }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/AbstractOpenCensusLog4jLogCorrelationTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/AbstractOpenCensusLog4jLogCorrelationTest.java
new file mode 100644
index 00000000..93ad85e0
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/AbstractOpenCensusLog4jLogCorrelationTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.log4j2;
+
+import io.opencensus.common.Function;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.Tracing;
+import java.io.StringWriter;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.StringLayout;
+import org.apache.logging.log4j.core.appender.WriterAppender;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+
+/**
+ * Superclass for all Log4j log correlation test classes.
+ *
+ * <p>The tests are split into multiple classes so that each one can be run with a different value
+ * for the system property {@link OpenCensusTraceContextDataInjector#SPAN_SELECTION_PROPERTY_NAME}.
+ * The property must be set when Log4j initializes a static variable, and running each test class in
+ * a separate JVM causes the static variable to be reinitialized.
+ */
+abstract class AbstractOpenCensusLog4jLogCorrelationTest {
+ private static final Tracer tracer = Tracing.getTracer();
+
+ static final String TEST_PATTERN =
+ "traceId=%X{opencensusTraceId} spanId=%X{opencensusSpanId} "
+ + "sampled=%X{opencensusTraceSampled} %-5level - %msg";
+
+ static final Tracestate EMPTY_TRACESTATE = Tracestate.builder().build();
+
+ private static Logger logger;
+
+ // This method initializes Log4j after setting the SpanSelection, which means that Log4j
+ // initializes a static variable with a ContextDataInjector that is constructed with the proper
+ // SpanSelection. This method should be called from a @BeforeClass method in each subclass.
+ static void initializeLog4j(SpanSelection spanSelection) {
+ System.setProperty(
+ OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME, spanSelection.toString());
+ logger = (Logger) LogManager.getLogger(AbstractOpenCensusLog4jLogCorrelationTest.class);
+ }
+
+ // Reconfigures Log4j using the given arguments and runs the function with the given SpanContext
+ // in scope.
+ String logWithSpanAndLog4jConfiguration(
+ String log4jPattern, SpanContext spanContext, Function<Logger, Void> loggingFunction) {
+ StringWriter output = new StringWriter();
+ StringLayout layout = PatternLayout.newBuilder().withPattern(log4jPattern).build();
+ Appender appender =
+ WriterAppender.newBuilder()
+ .setTarget(output)
+ .setLayout(layout)
+ .setName("TestAppender")
+ .build();
+ ((LoggerContext) LogManager.getContext(false)).updateLoggers();
+ appender.start();
+ logger.addAppender(appender);
+ logger.setLevel(Level.ALL);
+ try {
+ logWithSpan(spanContext, loggingFunction, logger);
+ return output.toString();
+ } finally {
+ logger.removeAppender(appender);
+ }
+ }
+
+ private static void logWithSpan(
+ SpanContext spanContext, Function<Logger, Void> loggingFunction, Logger logger) {
+ Scope scope = tracer.withSpan(new TestSpan(spanContext));
+ try {
+ loggingFunction.apply(logger);
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationAllSpansTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationAllSpansTest.java
new file mode 100644
index 00000000..355c9b67
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationAllSpansTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.log4j2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Function;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Log4j log correlation with {@link
+ * OpenCensusTraceContextDataInjector#SPAN_SELECTION_PROPERTY_NAME} set to {@link
+ * SpanSelection#ALL_SPANS}.
+ */
+@RunWith(JUnit4.class)
+public final class OpenCensusLog4jLogCorrelationAllSpansTest
+ extends AbstractOpenCensusLog4jLogCorrelationTest {
+
+ @BeforeClass
+ public static void setUp() {
+ initializeLog4j(SpanSelection.ALL_SPANS);
+ }
+
+ @Test
+ public void addSampledSpanToLogEntryWithAllSpans() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ TEST_PATTERN,
+ SpanContext.create(
+ TraceId.fromLowerBase16("b9718fe3d82d36fce0e6a1ada1c21db0"),
+ SpanId.fromLowerBase16("75159dde8c503fee"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE),
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ logger.warn("message #1");
+ return null;
+ }
+ });
+ assertThat(log)
+ .isEqualTo(
+ "traceId=b9718fe3d82d36fce0e6a1ada1c21db0 spanId=75159dde8c503fee "
+ + "sampled=true WARN - message #1");
+ }
+
+ @Test
+ public void addNonSampledSpanToLogEntryWithAllSpans() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ TEST_PATTERN,
+ SpanContext.create(
+ TraceId.fromLowerBase16("cd7061dfa9d312cdcc42edab3feab51b"),
+ SpanId.fromLowerBase16("117d42d4c7acd066"),
+ TraceOptions.builder().setIsSampled(false).build(),
+ EMPTY_TRACESTATE),
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ logger.info("message #2");
+ return null;
+ }
+ });
+ assertThat(log)
+ .isEqualTo(
+ "traceId=cd7061dfa9d312cdcc42edab3feab51b spanId=117d42d4c7acd066 sampled=false INFO "
+ + "- message #2");
+ }
+
+ @Test
+ public void addBlankSpanToLogEntryWithAllSpans() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ TEST_PATTERN,
+ SpanContext.INVALID,
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ logger.fatal("message #3");
+ return null;
+ }
+ });
+ assertThat(log)
+ .isEqualTo(
+ "traceId=00000000000000000000000000000000 spanId=0000000000000000 sampled=false FATAL "
+ + "- message #3");
+ }
+
+ @Test
+ public void preserveOtherKeyValuePairs() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ "%X{opencensusTraceId} %X{myTestKey} %-5level - %msg",
+ SpanContext.create(
+ TraceId.fromLowerBase16("c95329bb6b7de41afbc51a231c128f97"),
+ SpanId.fromLowerBase16("bf22ea74d38eddad"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE),
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ String key = "myTestKey";
+ ThreadContext.put(key, "myTestValue");
+ try {
+ logger.error("message #4");
+ } finally {
+ ThreadContext.remove(key);
+ }
+ return null;
+ }
+ });
+ assertThat(log).isEqualTo("c95329bb6b7de41afbc51a231c128f97 myTestValue ERROR - message #4");
+ }
+
+ @Test
+ public void overwriteExistingTracingKey() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ TEST_PATTERN,
+ SpanContext.create(
+ TraceId.fromLowerBase16("18e4ae44273a0c44e0c9ea4380792c66"),
+ SpanId.fromLowerBase16("199a7e16daa000a7"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE),
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ ThreadContext.put(
+ OpenCensusTraceContextDataInjector.TRACE_ID_CONTEXT_KEY, "existingTraceId");
+ try {
+ logger.error("message #5");
+ } finally {
+ ThreadContext.remove(OpenCensusTraceContextDataInjector.TRACE_ID_CONTEXT_KEY);
+ }
+ return null;
+ }
+ });
+ assertThat(log)
+ .isEqualTo(
+ "traceId=18e4ae44273a0c44e0c9ea4380792c66 spanId=199a7e16daa000a7 "
+ + "sampled=true ERROR - message #5");
+ }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationNoSpansTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationNoSpansTest.java
new file mode 100644
index 00000000..1205924e
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationNoSpansTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.log4j2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Function;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import org.apache.logging.log4j.core.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Log4j log correlation with {@link
+ * OpenCensusTraceContextDataInjector#SPAN_SELECTION_PROPERTY_NAME} set to {@link
+ * SpanSelection#NO_SPANS}.
+ */
+@RunWith(JUnit4.class)
+public final class OpenCensusLog4jLogCorrelationNoSpansTest
+ extends AbstractOpenCensusLog4jLogCorrelationTest {
+
+ @BeforeClass
+ public static void setUp() {
+ initializeLog4j(SpanSelection.NO_SPANS);
+ }
+
+ @Test
+ public void doNotAddSampledSpanToLogEntryWithNoSpans() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ TEST_PATTERN,
+ SpanContext.create(
+ TraceId.fromLowerBase16("03d2ada98f6eb8330605a45a88c7e67d"),
+ SpanId.fromLowerBase16("ce5b1cf09fe58bcb"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE),
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ logger.trace("message #1");
+ return null;
+ }
+ });
+ assertThat(log).isEqualTo("traceId= spanId= sampled= TRACE - message #1");
+ }
+
+ @Test
+ public void doNotAddNonSampledSpanToLogEntryWithNoSpans() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ TEST_PATTERN,
+ SpanContext.create(
+ TraceId.fromLowerBase16("09664283d189791de5218ffe3be88d54"),
+ SpanId.fromLowerBase16("a7203a50089a4029"),
+ TraceOptions.builder().setIsSampled(false).build(),
+ EMPTY_TRACESTATE),
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ logger.warn("message #2");
+ return null;
+ }
+ });
+ assertThat(log).isEqualTo("traceId= spanId= sampled= WARN - message #2");
+ }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationSampledSpansTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationSampledSpansTest.java
new file mode 100644
index 00000000..bbce4135
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusLog4jLogCorrelationSampledSpansTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.log4j2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Function;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import org.apache.logging.log4j.core.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Log4j log correlation with {@link
+ * OpenCensusTraceContextDataInjector#SPAN_SELECTION_PROPERTY_NAME} set to {@link
+ * SpanSelection#SAMPLED_SPANS}.
+ */
+@RunWith(JUnit4.class)
+public final class OpenCensusLog4jLogCorrelationSampledSpansTest
+ extends AbstractOpenCensusLog4jLogCorrelationTest {
+
+ @BeforeClass
+ public static void setUp() {
+ initializeLog4j(SpanSelection.SAMPLED_SPANS);
+ }
+
+ @Test
+ public void addSampledSpanToLogEntryWithSampledSpans() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ TEST_PATTERN,
+ SpanContext.create(
+ TraceId.fromLowerBase16("0af7a7bef890695f1c5e85a8e7290164"),
+ SpanId.fromLowerBase16("d3f07c467ec2fbb2"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE),
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ logger.error("message #1");
+ return null;
+ }
+ });
+ assertThat(log)
+ .isEqualTo(
+ "traceId=0af7a7bef890695f1c5e85a8e7290164 spanId=d3f07c467ec2fbb2 sampled=true ERROR "
+ + "- message #1");
+ }
+
+ @Test
+ public void doNotAddNonSampledSpanToLogEntryWithSampledSpans() {
+ String log =
+ logWithSpanAndLog4jConfiguration(
+ TEST_PATTERN,
+ SpanContext.create(
+ TraceId.fromLowerBase16("9e09b559ebb8f7f7ed7451aff68cf441"),
+ SpanId.fromLowerBase16("0fc9ef54c50a1816"),
+ TraceOptions.builder().setIsSampled(false).build(),
+ EMPTY_TRACESTATE),
+ new Function<Logger, Void>() {
+ @Override
+ public Void apply(Logger logger) {
+ logger.debug("message #2");
+ return null;
+ }
+ });
+ assertThat(log).isEqualTo("traceId= spanId= sampled= DEBUG - message #2");
+ }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjectorTest.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjectorTest.java
new file mode 100644
index 00000000..3b704058
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/OpenCensusTraceContextDataInjectorTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.log4j2;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.logcorrelation.log4j2.OpenCensusTraceContextDataInjector.SpanSelection;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.Tracing;
+import java.util.Collections;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link OpenCensusTraceContextDataInjector}. */
+@RunWith(JUnit4.class)
+public final class OpenCensusTraceContextDataInjectorTest {
+ static final Tracestate EMPTY_TRACESTATE = Tracestate.builder().build();
+
+ private final Tracer tracer = Tracing.getTracer();
+
+ @Test
+ @SuppressWarnings("TruthConstantAsserts")
+ public void spanSelectionPropertyName() {
+ assertThat(OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME)
+ .isEqualTo(OpenCensusTraceContextDataInjector.class.getName() + ".spanSelection");
+ }
+
+ @Test
+ public void traceIdKey() {
+ assertThat(OpenCensusTraceContextDataInjector.TRACE_ID_CONTEXT_KEY)
+ .isEqualTo("opencensusTraceId");
+ }
+
+ @Test
+ public void spanIdKey() {
+ assertThat(OpenCensusTraceContextDataInjector.SPAN_ID_CONTEXT_KEY)
+ .isEqualTo("opencensusSpanId");
+ }
+
+ @Test
+ public void traceSampledKey() {
+ assertThat(OpenCensusTraceContextDataInjector.TRACE_SAMPLED_CONTEXT_KEY)
+ .isEqualTo("opencensusTraceSampled");
+ }
+
+ @Test
+ public void spanSelectionDefaultIsAllSpans() {
+ assertThat(new OpenCensusTraceContextDataInjector().getSpanSelection())
+ .isEqualTo(SpanSelection.ALL_SPANS);
+ }
+
+ @Test
+ public void setSpanSelectionWithSystemProperty() {
+ try {
+ System.setProperty(
+ OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME, "NO_SPANS");
+ assertThat(new OpenCensusTraceContextDataInjector().getSpanSelection())
+ .isEqualTo(SpanSelection.NO_SPANS);
+ } finally {
+ System.clearProperty(OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME);
+ }
+ }
+
+ @Test
+ public void useDefaultValueForInvalidSpanSelection() {
+ try {
+ System.setProperty(
+ OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME,
+ "INVALID_SPAN_SELECTION");
+ assertThat(new OpenCensusTraceContextDataInjector().getSpanSelection())
+ .isEqualTo(SpanSelection.ALL_SPANS);
+ } finally {
+ System.clearProperty(OpenCensusTraceContextDataInjector.SPAN_SELECTION_PROPERTY_NAME);
+ }
+ }
+
+ @Test
+ public void insertConfigurationProperties() {
+ assertThat(
+ new OpenCensusTraceContextDataInjector(SpanSelection.ALL_SPANS)
+ .injectContextData(
+ Lists.newArrayList(
+ Property.createProperty("property1", "value1"),
+ Property.createProperty("property2", "value2")),
+ new SortedArrayStringMap())
+ .toMap())
+ .containsExactly(
+ "property1",
+ "value1",
+ "property2",
+ "value2",
+ "opencensusTraceId",
+ "00000000000000000000000000000000",
+ "opencensusSpanId",
+ "0000000000000000",
+ "opencensusTraceSampled",
+ "false");
+ }
+
+ @Test
+ public void handleEmptyConfigurationProperties() {
+ assertContainsOnlyDefaultTracingEntries(
+ new OpenCensusTraceContextDataInjector(SpanSelection.ALL_SPANS)
+ .injectContextData(Collections.<Property>emptyList(), new SortedArrayStringMap()));
+ }
+
+ @Test
+ public void handleNullConfigurationProperties() {
+ assertContainsOnlyDefaultTracingEntries(
+ new OpenCensusTraceContextDataInjector(SpanSelection.ALL_SPANS)
+ .injectContextData(null, new SortedArrayStringMap()));
+ }
+
+ private static void assertContainsOnlyDefaultTracingEntries(StringMap stringMap) {
+ assertThat(stringMap.toMap())
+ .containsExactly(
+ "opencensusTraceId",
+ "00000000000000000000000000000000",
+ "opencensusSpanId",
+ "0000000000000000",
+ "opencensusTraceSampled",
+ "false");
+ }
+
+ @Test
+ public void rawContextDataWithTracingData() {
+ OpenCensusTraceContextDataInjector plugin =
+ new OpenCensusTraceContextDataInjector(SpanSelection.ALL_SPANS);
+ SpanContext spanContext =
+ SpanContext.create(
+ TraceId.fromLowerBase16("e17944156660f55b8cae5ce3f45d4a40"),
+ SpanId.fromLowerBase16("fc3d2ba0d283b66a"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE);
+ Scope scope = tracer.withSpan(new TestSpan(spanContext));
+ try {
+ String key = "myTestKey";
+ ThreadContext.put(key, "myTestValue");
+ try {
+ assertThat(plugin.rawContextData().toMap())
+ .containsExactly(
+ "myTestKey",
+ "myTestValue",
+ "opencensusTraceId",
+ "e17944156660f55b8cae5ce3f45d4a40",
+ "opencensusSpanId",
+ "fc3d2ba0d283b66a",
+ "opencensusTraceSampled",
+ "true");
+ } finally {
+ ThreadContext.remove(key);
+ }
+ } finally {
+ scope.close();
+ }
+ }
+
+ @Test
+ public void rawContextDataWithoutTracingData() {
+ OpenCensusTraceContextDataInjector plugin =
+ new OpenCensusTraceContextDataInjector(SpanSelection.NO_SPANS);
+ SpanContext spanContext =
+ SpanContext.create(
+ TraceId.fromLowerBase16("ea236000f6d387fe7c06c5a6d6458b53"),
+ SpanId.fromLowerBase16("f3b39dbbadb73074"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE);
+ Scope scope = tracer.withSpan(new TestSpan(spanContext));
+ try {
+ String key = "myTestKey";
+ ThreadContext.put(key, "myTestValue");
+ try {
+ assertThat(plugin.rawContextData().toMap()).containsExactly("myTestKey", "myTestValue");
+ } finally {
+ ThreadContext.remove(key);
+ }
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/TestSpan.java b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/TestSpan.java
new file mode 100644
index 00000000..7af46064
--- /dev/null
+++ b/contrib/log_correlation/log4j2/src/test/java/io/opencensus/contrib/logcorrelation/log4j2/TestSpan.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.log4j2;
+
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import java.util.EnumSet;
+import java.util.Map;
+
+// Simple test Span that holds a SpanContext. The tests cannot use Span directly, since it is
+// abstract.
+final class TestSpan extends Span {
+ TestSpan(SpanContext context) {
+ super(context, EnumSet.of(Options.RECORD_EVENTS));
+ }
+
+ @Override
+ public void end(EndSpanOptions options) {}
+
+ @Override
+ public void addLink(Link link) {}
+
+ @Override
+ public void addAnnotation(Annotation annotation) {}
+
+ @Override
+ public void addAnnotation(String description, Map<String, AttributeValue> attributes) {}
+}
diff --git a/contrib/log_correlation/stackdriver/README.md b/contrib/log_correlation/stackdriver/README.md
new file mode 100644
index 00000000..8d99ff20
--- /dev/null
+++ b/contrib/log_correlation/stackdriver/README.md
@@ -0,0 +1,147 @@
+# OpenCensus Stackdriver Log Correlation
+
+This subproject is currently experimental, so it may be redesigned or removed in the future. It
+will remain experimental until we have a specification for a log correlation feature in
+[opencensus-specs](https://github.com/census-instrumentation/opencensus-specs/)
+(issue [#123](https://github.com/census-instrumentation/opencensus-specs/issues/123)).
+
+The `opencensus-contrib-log-correlation-stackdriver` artifact provides a
+[Stackdriver Logging](https://cloud.google.com/logging/)
+[`LoggingEnhancer`](http://googlecloudplatform.github.io/google-cloud-java/google-cloud-clients/apidocs/com/google/cloud/logging/LoggingEnhancer.html)
+that automatically adds tracing data to log entries. The class name is
+`OpenCensusTraceLoggingEnhancer`. `OpenCensusTraceLoggingEnhancer` adds the current trace and span
+ID to each log entry, which allows Stackdriver to display the log entries associated with each
+trace, or filter logs based on trace or span ID. It currently also adds the sampling decision using
+the label "`opencensusTraceSampled`".
+
+## Instructions
+
+### Prerequisites
+
+This log correlation feature requires a project that is using the
+[`com.google.cloud:google-cloud-logging`](https://github.com/GoogleCloudPlatform/google-cloud-java/tree/master/google-cloud-clients/google-cloud-logging)
+library to export logs to Stackdriver. `google-cloud-logging` must be version `1.33.0` or later.
+The application can run on Google Cloud Platform, on-premise, or on
+another cloud platform. See https://cloud.google.com/logging/docs/setup/java for instructions for
+setting up `google-cloud-logging`.
+
+**Note that this artifact does not support logging done through the Stackdriver Logging agent.**
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-log-correlation-stackdriver</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+runtime 'io.opencensus:opencensus-contrib-log-correlation-stackdriver:0.16.1'
+```
+
+### Configure the `OpenCensusTraceLoggingEnhancer`
+
+#### Setting the project ID
+
+By default, `OpenCensusTraceLoggingEnhancer` looks up the project ID from `google-cloud-java`. See
+[here](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id) for
+instructions for configuring the project ID with `google-cloud-java`.
+
+To override the project ID, set the following property as a system property or as a
+`java.util.logging` property:
+
+`io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.projectId`
+
+#### Choosing when to add tracing data to log entries
+
+The following property controls the decision to add tracing data from the current span to a log
+entry:
+
+`io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.spanSelection`
+
+The allowed values are:
+
+* `ALL_SPANS`: adds tracing data to all log entries (default)
+
+* `NO_SPANS`: disables the log correlation feature
+
+* `SAMPLED_SPANS`: adds tracing data to log entries when the current span is sampled
+
+Other aspects of configuring the `OpenCensusTraceLoggingEnhancer` depend on the logging
+implementation and `google-cloud-logging` adapter in use.
+
+#### Logback with `google-cloud-logging-logback` `LoggingAppender`
+
+The `LoggingAppender` should already be configured in `logback.xml` as described in
+https://cloud.google.com/logging/docs/setup/java#logback_appender. Add
+"`io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer`" to the list of
+enhancers. Optionally, set the `spanSelection` and `projectId` properties described above as system
+properties.
+
+Here is an example `logback.xml`, based on the
+[`google-cloud-logging-logback` example](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/a2b04b20d81ee631439a9368fb99b44849519e28/logging/logback/src/main/resources/logback.xml).
+It specifies the `LoggingEnhancer` class and sets both optional properties:
+
+```xml
+<configuration>
+ <property scope="system" name="io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.spanSelection" value="SAMPLED_SPANS" />
+ <property scope="system" name="io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.projectId" value="my-project-id" />
+ <appender name="CLOUD" class="com.google.cloud.logging.logback.LoggingAppender">
+ <enhancer>io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer</enhancer>
+ </appender>
+
+ <root level="info">
+ <appender-ref ref="CLOUD" />
+ </root>
+</configuration>
+```
+
+See
+https://github.com/census-ecosystem/opencensus-experiments/tree/master/java/log_correlation/stackdriver/logback
+for a full example.
+
+#### `java.util.logging` with `google-cloud-logging` `LoggingHandler`
+
+The `LoggingHandler` should already be configured in a logging `.properties` file, as described in
+https://cloud.google.com/logging/docs/setup/java#jul_handler. Add
+"`io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer`" to the list of
+enhancers. Optionally, set the `spanSelection` and `projectId` properties described above in the
+properties file.
+
+Here is an example `.properties` file, based on the
+[`google-cloud-logging` example](https://github.com/GoogleCloudPlatform/java-docs-samples/blob/a2b04b20d81ee631439a9368fb99b44849519e28/logging/jul/src/main/resources/logging.properties).
+It specifies the `LoggingEnhancer` class and sets both optional properties:
+
+```properties
+.level = INFO
+
+com.example.MyClass.handlers=com.google.cloud.logging.LoggingHandler
+
+com.google.cloud.logging.LoggingHandler.enhancers=io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer
+io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.spanSelection=SAMPLED_SPANS
+io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.projectId=my-project-id
+```
+
+See
+https://github.com/census-ecosystem/opencensus-experiments/tree/master/java/log_correlation/stackdriver/java_util_logging
+for a full example.
+
+#### Custom `google-cloud-logging` adapter
+
+The `google-cloud-logging` adapter needs to instantiate the `OpenCensusTraceLoggingEnhancer`,
+possibly by looking up the class name of the `LoggingEnhancer` in a configuration file and
+instantiating it with reflection. Then the adapter needs to call the `LoggingEnhancer`'s
+`enhanceLogEntry` method on all `LogEntry`s that will be passed to `google-cloud-logging`'s
+`Logging.write` method. `enhanceLogEntry` must be called in the same thread that executed the log
+statement, in order to provide the current trace and span ID.
+
+#### Java Versions
+
+Java 7 or above is required for using this artifact.
diff --git a/contrib/log_correlation/stackdriver/build.gradle b/contrib/log_correlation/stackdriver/build.gradle
new file mode 100644
index 00000000..4d8a2985
--- /dev/null
+++ b/contrib/log_correlation/stackdriver/build.gradle
@@ -0,0 +1,13 @@
+description = 'OpenCensus Stackdriver Log Correlation'
+
+apply plugin: 'java'
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.google_cloud_logging
+
+ testCompile libraries.guava
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/log_correlation/stackdriver/src/main/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancer.java b/contrib/log_correlation/stackdriver/src/main/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancer.java
new file mode 100644
index 00000000..5c3e21ff
--- /dev/null
+++ b/contrib/log_correlation/stackdriver/src/main/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancer.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.stackdriver;
+
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.logging.LogEntry;
+import com.google.cloud.logging.LoggingEnhancer;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.unsafe.ContextUtils;
+import java.util.logging.LogManager;
+import javax.annotation.Nullable;
+
+/**
+ * Stackdriver {@link LoggingEnhancer} that adds OpenCensus tracing data to log entries.
+ *
+ * <p>This feature is currently experimental.
+ *
+ * @since 0.15
+ */
+@ExperimentalApi
+public final class OpenCensusTraceLoggingEnhancer implements LoggingEnhancer {
+ private static final String SAMPLED_LABEL_KEY = "opencensusTraceSampled";
+ private static final SpanSelection DEFAULT_SPAN_SELECTION = SpanSelection.ALL_SPANS;
+
+ /**
+ * Name of the property that overrides the default project ID (overrides the value returned by
+ * {@code com.google.cloud.ServiceOptions.getDefaultProjectId()}). The name is {@value}.
+ *
+ * @since 0.15
+ */
+ public static final String PROJECT_ID_PROPERTY_NAME =
+ "io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.projectId";
+
+ /**
+ * Name of the property that defines the {@link SpanSelection}. The name is {@value}.
+ *
+ * @since 0.15
+ */
+ public static final String SPAN_SELECTION_PROPERTY_NAME =
+ "io.opencensus.contrib.logcorrelation.stackdriver."
+ + "OpenCensusTraceLoggingEnhancer.spanSelection";
+
+ private final String projectId;
+ private final SpanSelection spanSelection;
+
+ // This field caches the prefix used for the LogEntry.trace field and is derived from projectId.
+ private final String tracePrefix;
+
+ /**
+ * How to decide whether to add tracing data from the current span to a log entry.
+ *
+ * @since 0.15
+ */
+ public enum SpanSelection {
+
+ /**
+ * Never add tracing data to log entries. This constant disables the log correlation feature.
+ *
+ * @since 0.15
+ */
+ NO_SPANS,
+
+ /**
+ * Add tracing data to a log entry iff the current span is sampled.
+ *
+ * @since 0.15
+ */
+ SAMPLED_SPANS,
+
+ /**
+ * Always add tracing data to log entries, even when the current span is not sampled. This is
+ * the default.
+ *
+ * @since 0.15
+ */
+ ALL_SPANS
+ }
+
+ /**
+ * Constructor to be called by reflection, e.g., by a google-cloud-java {@code LoggingHandler} or
+ * google-cloud-logging-logback {@code LoggingAppender}.
+ *
+ * <p>This constructor looks up the project ID and {@link SpanSelection SpanSelection} from the
+ * environment. It uses the default project ID (the value returned by {@code
+ * com.google.cloud.ServiceOptions.getDefaultProjectId()}), unless the ID is overridden by the
+ * property {@value #PROJECT_ID_PROPERTY_NAME}. It looks up the {@code SpanSelection} using the
+ * property {@value #SPAN_SELECTION_PROPERTY_NAME}. Each property can be specified with a {@link
+ * java.util.logging} property or a system property, with preference given to the logging
+ * property.
+ *
+ * @since 0.15
+ */
+ public OpenCensusTraceLoggingEnhancer() {
+ this(lookUpProjectId(), lookUpSpanSelectionProperty());
+ }
+
+ /**
+ * Constructs a {@code OpenCensusTraceLoggingEnhancer} with the given project ID and {@code
+ * SpanSelection}.
+ *
+ * @param projectId the project ID for this instance.
+ * @param spanSelection the {@code SpanSelection} for this instance.
+ * @since 0.15
+ */
+ public OpenCensusTraceLoggingEnhancer(@Nullable String projectId, SpanSelection spanSelection) {
+ this.projectId = projectId == null ? "" : projectId;
+ this.spanSelection = spanSelection;
+ this.tracePrefix = "projects/" + this.projectId + "/traces/";
+ }
+
+ private static String lookUpProjectId() {
+ String projectIdProperty = lookUpProperty(PROJECT_ID_PROPERTY_NAME);
+ return projectIdProperty == null || projectIdProperty.isEmpty()
+ ? ServiceOptions.getDefaultProjectId()
+ : projectIdProperty;
+ }
+
+ private static SpanSelection lookUpSpanSelectionProperty() {
+ String spanSelectionProperty = lookUpProperty(SPAN_SELECTION_PROPERTY_NAME);
+ return spanSelectionProperty == null || spanSelectionProperty.isEmpty()
+ ? DEFAULT_SPAN_SELECTION
+ : parseSpanSelection(spanSelectionProperty);
+ }
+
+ private static SpanSelection parseSpanSelection(String spanSelection) {
+ try {
+ return SpanSelection.valueOf(spanSelection);
+ } catch (IllegalArgumentException e) {
+ return DEFAULT_SPAN_SELECTION;
+ }
+ }
+
+ // An OpenCensusTraceLoggingEnhancer property can be set with a logging property or a system
+ // property.
+ @Nullable
+ private static String lookUpProperty(String name) {
+ String property = LogManager.getLogManager().getProperty(name);
+ return property == null || property.isEmpty() ? System.getProperty(name) : property;
+ }
+
+ /**
+ * Returns the project ID setting for this instance.
+ *
+ * @return the project ID setting for this instance.
+ * @since 0.15
+ */
+ public String getProjectId() {
+ return projectId;
+ }
+
+ /**
+ * Returns the {@code SpanSelection} setting for this instance.
+ *
+ * @return the {@code SpanSelection} setting for this instance.
+ * @since 0.15
+ */
+ public SpanSelection getSpanSelection() {
+ return spanSelection;
+ }
+
+ // This method avoids getting the current span when the feature is disabled, for efficiency.
+ @Override
+ public void enhanceLogEntry(LogEntry.Builder builder) {
+ switch (spanSelection) {
+ case NO_SPANS:
+ return;
+ case SAMPLED_SPANS:
+ SpanContext span = getCurrentSpanContext();
+ if (span.getTraceOptions().isSampled()) {
+ addTracingData(tracePrefix, span, builder);
+ }
+ return;
+ case ALL_SPANS:
+ addTracingData(tracePrefix, getCurrentSpanContext(), builder);
+ return;
+ }
+ throw new AssertionError("Unknown spanSelection: " + spanSelection);
+ }
+
+ private static SpanContext getCurrentSpanContext() {
+ Span span = ContextUtils.CONTEXT_SPAN_KEY.get();
+ return span == null ? SpanContext.INVALID : span.getContext();
+ }
+
+ private static void addTracingData(
+ String tracePrefix, SpanContext span, LogEntry.Builder builder) {
+ builder.setTrace(formatTraceId(tracePrefix, span.getTraceId()));
+ builder.setSpanId(span.getSpanId().toLowerBase16());
+
+ // TODO(sebright): Find the correct way to add the sampling decision.
+ builder.addLabel(SAMPLED_LABEL_KEY, Boolean.toString(span.getTraceOptions().isSampled()));
+ }
+
+ private static String formatTraceId(String tracePrefix, TraceId traceId) {
+ return tracePrefix + traceId.toLowerBase16();
+ }
+}
diff --git a/contrib/log_correlation/stackdriver/src/test/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancerTest.java b/contrib/log_correlation/stackdriver/src/test/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancerTest.java
new file mode 100644
index 00000000..c116f09a
--- /dev/null
+++ b/contrib/log_correlation/stackdriver/src/test/java/io/opencensus/contrib/logcorrelation/stackdriver/OpenCensusTraceLoggingEnhancerTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2018, 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.contrib.logcorrelation.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.logging.LogEntry;
+import com.google.cloud.logging.LoggingEnhancer;
+import com.google.common.base.Charsets;
+import com.google.common.io.CharSource;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.logcorrelation.stackdriver.OpenCensusTraceLoggingEnhancer.SpanSelection;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.Tracing;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.logging.LogManager;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for {@link OpenCensusTraceLoggingEnhancer}. */
+// TODO(sebright): Find a way to test that OpenCensusTraceLoggingEnhancer is called from Stackdriver
+// logging. See
+// https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/TESTING.md#testing-code-that-uses-logging.
+@RunWith(JUnit4.class)
+public class OpenCensusTraceLoggingEnhancerTest {
+ private static final String GOOGLE_CLOUD_PROJECT = "GOOGLE_CLOUD_PROJECT";
+ private static final Tracestate EMPTY_TRACESTATE = Tracestate.builder().build();
+
+ private static final Tracer tracer = Tracing.getTracer();
+
+ @Test
+ public void enhanceLogEntry_DoNotAddSampledSpanToLogEntryWithNoSpans() {
+ LogEntry logEntry =
+ getEnhancedLogEntry(
+ new OpenCensusTraceLoggingEnhancer("my-test-project-1", SpanSelection.NO_SPANS),
+ new TestSpan(
+ SpanContext.create(
+ TraceId.fromLowerBase16("3da31be987098abb08c71c7700d2680e"),
+ SpanId.fromLowerBase16("51b109f15e0d3881"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE)));
+ assertContainsNoTracingData(logEntry);
+ }
+
+ @Test
+ public void enhanceLogEntry_AddSampledSpanToLogEntryWithSampledSpans() {
+ LogEntry logEntry =
+ getEnhancedLogEntry(
+ new OpenCensusTraceLoggingEnhancer("my-test-project-2", SpanSelection.SAMPLED_SPANS),
+ new TestSpan(
+ SpanContext.create(
+ TraceId.fromLowerBase16("4c9874d0b41224cce77ff74ee10f5ee6"),
+ SpanId.fromLowerBase16("592ae363e92cb3dd"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE)));
+ assertThat(logEntry.getLabels()).containsEntry("opencensusTraceSampled", "true");
+ assertThat(logEntry.getTrace())
+ .isEqualTo("projects/my-test-project-2/traces/4c9874d0b41224cce77ff74ee10f5ee6");
+ assertThat(logEntry.getSpanId()).isEqualTo("592ae363e92cb3dd");
+ }
+
+ @Test
+ public void enhanceLogEntry_AddSampledSpanToLogEntryWithAllSpans() {
+ LogEntry logEntry =
+ getEnhancedLogEntry(
+ new OpenCensusTraceLoggingEnhancer("my-test-project-3", SpanSelection.ALL_SPANS),
+ new TestSpan(
+ SpanContext.create(
+ TraceId.fromLowerBase16("4c6af40c499951eb7de2777ba1e4fefa"),
+ SpanId.fromLowerBase16("de52e84d13dd232d"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE)));
+ assertThat(logEntry.getLabels()).containsEntry("opencensusTraceSampled", "true");
+ assertThat(logEntry.getTrace())
+ .isEqualTo("projects/my-test-project-3/traces/4c6af40c499951eb7de2777ba1e4fefa");
+ assertThat(logEntry.getSpanId()).isEqualTo("de52e84d13dd232d");
+ }
+
+ @Test
+ public void enhanceLogEntry_DoNotAddNonSampledSpanToLogEntryWithNoSpans() {
+ LogEntry logEntry =
+ getEnhancedLogEntry(
+ new OpenCensusTraceLoggingEnhancer("my-test-project-4", SpanSelection.NO_SPANS),
+ new TestSpan(
+ SpanContext.create(
+ TraceId.fromLowerBase16("88ab22b18b97369df065ca830e41cf6a"),
+ SpanId.fromLowerBase16("8987d372039021fd"),
+ TraceOptions.builder().setIsSampled(false).build(),
+ EMPTY_TRACESTATE)));
+ assertContainsNoTracingData(logEntry);
+ }
+
+ @Test
+ public void enhanceLogEntry_DoNotAddNonSampledSpanToLogEntryWithSampledSpans() {
+ LogEntry logEntry =
+ getEnhancedLogEntry(
+ new OpenCensusTraceLoggingEnhancer("my-test-project-5", SpanSelection.SAMPLED_SPANS),
+ new TestSpan(
+ SpanContext.create(
+ TraceId.fromLowerBase16("7f4703d9bb02f4f2e67fb840103cdd34"),
+ SpanId.fromLowerBase16("2d7d95a555557434"),
+ TraceOptions.builder().setIsSampled(false).build(),
+ EMPTY_TRACESTATE)));
+ assertContainsNoTracingData(logEntry);
+ }
+
+ @Test
+ public void enhanceLogEntry_AddNonSampledSpanToLogEntryWithAllSpans() {
+ LogEntry logEntry =
+ getEnhancedLogEntry(
+ new OpenCensusTraceLoggingEnhancer("my-test-project-6", SpanSelection.ALL_SPANS),
+ new TestSpan(
+ SpanContext.create(
+ TraceId.fromLowerBase16("72c905c76f99e99974afd84dc053a480"),
+ SpanId.fromLowerBase16("731e102335b7a5a0"),
+ TraceOptions.builder().setIsSampled(false).build(),
+ EMPTY_TRACESTATE)));
+ assertThat(logEntry.getLabels()).containsEntry("opencensusTraceSampled", "false");
+ assertThat(logEntry.getTrace())
+ .isEqualTo("projects/my-test-project-6/traces/72c905c76f99e99974afd84dc053a480");
+ assertThat(logEntry.getSpanId()).isEqualTo("731e102335b7a5a0");
+ }
+
+ @Test
+ public void enhanceLogEntry_AddBlankSpanToLogEntryWithAllSpans() {
+ LogEntry logEntry =
+ getEnhancedLogEntry(
+ new OpenCensusTraceLoggingEnhancer("my-test-project-7", SpanSelection.ALL_SPANS),
+ BlankSpan.INSTANCE);
+ assertThat(logEntry.getLabels().get("opencensusTraceSampled")).isEqualTo("false");
+ assertThat(logEntry.getTrace())
+ .isEqualTo("projects/my-test-project-7/traces/00000000000000000000000000000000");
+ assertThat(logEntry.getSpanId()).isEqualTo("0000000000000000");
+ }
+
+ @Test
+ public void enhanceLogEntry_ConvertNullProjectIdToEmptyString() {
+ LogEntry logEntry =
+ getEnhancedLogEntry(
+ new OpenCensusTraceLoggingEnhancer(null, SpanSelection.ALL_SPANS),
+ new TestSpan(
+ SpanContext.create(
+ TraceId.fromLowerBase16("bfb4248a24325a905873a1d43001d9a0"),
+ SpanId.fromLowerBase16("6f23f9afd448e272"),
+ TraceOptions.builder().setIsSampled(true).build(),
+ EMPTY_TRACESTATE)));
+ assertThat(logEntry.getTrace()).isEqualTo("projects//traces/bfb4248a24325a905873a1d43001d9a0");
+ }
+
+ private static LogEntry getEnhancedLogEntry(LoggingEnhancer loggingEnhancer, Span span) {
+ Scope scope = tracer.withSpan(span);
+ try {
+ LogEntry.Builder builder = LogEntry.newBuilder(null);
+ loggingEnhancer.enhanceLogEntry(builder);
+ return builder.build();
+ } finally {
+ scope.close();
+ }
+ }
+
+ private static void assertContainsNoTracingData(LogEntry logEntry) {
+ assertThat(logEntry.getLabels()).doesNotContainKey("opencensusTraceSampled");
+ assertThat(logEntry.getTrace()).isNull();
+ assertThat(logEntry.getSpanId()).isNull();
+ }
+
+ @Test
+ public void spanSelectionDefaultIsAllSpans() {
+ assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+ .isEqualTo(SpanSelection.ALL_SPANS);
+ }
+
+ @Test
+ @SuppressWarnings("TruthConstantAsserts")
+ public void projectIdPropertyName() {
+ assertThat(OpenCensusTraceLoggingEnhancer.PROJECT_ID_PROPERTY_NAME)
+ .isEqualTo(OpenCensusTraceLoggingEnhancer.class.getName() + ".projectId");
+ }
+
+ @Test
+ @SuppressWarnings("TruthConstantAsserts")
+ public void spanSelectionPropertyName() {
+ assertThat(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME)
+ .isEqualTo(OpenCensusTraceLoggingEnhancer.class.getName() + ".spanSelection");
+ }
+
+ @Test
+ public void setProjectIdWithGoogleCloudJava() {
+ try {
+ System.setProperty(GOOGLE_CLOUD_PROJECT, "my-project-id");
+ assertThat(new OpenCensusTraceLoggingEnhancer().getProjectId()).isEqualTo("my-project-id");
+ } finally {
+ System.clearProperty(GOOGLE_CLOUD_PROJECT);
+ }
+ }
+
+ @Test
+ public void overrideProjectIdWithSystemProperty() {
+ try {
+ System.setProperty(
+ OpenCensusTraceLoggingEnhancer.PROJECT_ID_PROPERTY_NAME, "project ID override");
+ try {
+ System.setProperty(GOOGLE_CLOUD_PROJECT, "GOOGLE_CLOUD_PROJECT project ID");
+ assertThat(new OpenCensusTraceLoggingEnhancer().getProjectId())
+ .isEqualTo("project ID override");
+ } finally {
+ System.clearProperty(GOOGLE_CLOUD_PROJECT);
+ }
+ } finally {
+ System.clearProperty(OpenCensusTraceLoggingEnhancer.PROJECT_ID_PROPERTY_NAME);
+ }
+ }
+
+ @Test
+ public void setSpanSelectionWithSystemProperty() {
+ try {
+ System.setProperty(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME, "NO_SPANS");
+ assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+ .isEqualTo(SpanSelection.NO_SPANS);
+ } finally {
+ System.clearProperty(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME);
+ }
+ }
+
+ @Test
+ public void overrideProjectIdWithLoggingProperty() throws IOException {
+ try {
+ LogManager.getLogManager()
+ .readConfiguration(
+ stringToInputStream(
+ OpenCensusTraceLoggingEnhancer.PROJECT_ID_PROPERTY_NAME + "=PROJECT_OVERRIDE"));
+ try {
+ System.setProperty(GOOGLE_CLOUD_PROJECT, "GOOGLE_CLOUD_PROJECT project ID");
+ assertThat(new OpenCensusTraceLoggingEnhancer().getProjectId())
+ .isEqualTo("PROJECT_OVERRIDE");
+ } finally {
+ System.clearProperty(GOOGLE_CLOUD_PROJECT);
+ }
+ } finally {
+ LogManager.getLogManager().reset();
+ }
+ }
+
+ @Test
+ public void setSpanSelectionWithLoggingProperty() throws IOException {
+ try {
+ LogManager.getLogManager()
+ .readConfiguration(
+ stringToInputStream(
+ OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME + "=SAMPLED_SPANS"));
+ assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+ .isEqualTo(SpanSelection.SAMPLED_SPANS);
+ } finally {
+ LogManager.getLogManager().reset();
+ }
+ }
+
+ @Test
+ public void loggingPropertyTakesPrecedenceOverSystemProperty() throws IOException {
+ try {
+ LogManager.getLogManager()
+ .readConfiguration(
+ stringToInputStream(
+ OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME + "=NO_SPANS"));
+ try {
+ System.setProperty(
+ OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME, "SAMPLED_SPANS");
+ assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+ .isEqualTo(SpanSelection.NO_SPANS);
+ } finally {
+ System.clearProperty(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME);
+ }
+ } finally {
+ LogManager.getLogManager().reset();
+ }
+ }
+
+ private static InputStream stringToInputStream(String contents) throws IOException {
+ return CharSource.wrap(contents).asByteSource(Charsets.UTF_8).openBufferedStream();
+ }
+
+ @Test
+ public void useDefaultValueForInvalidSpanSelection() {
+ try {
+ System.setProperty(
+ OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME, "INVALID_SPAN_SELECTION");
+ assertThat(new OpenCensusTraceLoggingEnhancer().getSpanSelection())
+ .isEqualTo(SpanSelection.ALL_SPANS);
+ } finally {
+ System.clearProperty(OpenCensusTraceLoggingEnhancer.SPAN_SELECTION_PROPERTY_NAME);
+ }
+ }
+
+ private static final class TestSpan extends Span {
+ TestSpan(SpanContext context) {
+ super(context, EnumSet.of(Options.RECORD_EVENTS));
+ }
+
+ @Override
+ public void end(EndSpanOptions options) {}
+
+ @Override
+ public void addLink(Link link) {}
+
+ @Override
+ public void addAnnotation(Annotation annotation) {}
+
+ @Override
+ public void addAnnotation(String description, Map<String, AttributeValue> attributes) {}
+ }
+}
diff --git a/contrib/monitored_resource_util/README.md b/contrib/monitored_resource_util/README.md
new file mode 100644
index 00000000..9d3c754c
--- /dev/null
+++ b/contrib/monitored_resource_util/README.md
@@ -0,0 +1,34 @@
+# OpenCensus Monitored Resources Util
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Monitored Resource Util for Java* is a collection of utilities for auto detecting
+monitored resource when exporting stats, based on the environment where the application is running.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-monitored-resource-util</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-monitored-resource-util:0.16.1'
+```
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-monitoredresource-util/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-monitoredresource-util
diff --git a/contrib/monitored_resource_util/build.gradle b/contrib/monitored_resource_util/build.gradle
new file mode 100644
index 00000000..1e25c7cf
--- /dev/null
+++ b/contrib/monitored_resource_util/build.gradle
@@ -0,0 +1,15 @@
+description = 'OpenCensus Monitored Resource Util'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtils.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtils.java
new file mode 100644
index 00000000..03b0bd4d
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtils.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2018, 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.contrib.monitoredresource.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.concurrent.GuardedBy;
+
+/** Util methods for getting and parsing AWS instance identity document. */
+final class AwsIdentityDocUtils {
+
+ private static final Object monitor = new Object();
+ private static final int AWS_IDENTITY_DOC_BUF_SIZE = 0x800; // 2K chars (4K bytes)
+ private static final String AWS_IDENTITY_DOC_LINE_BREAK_SPLITTER = "\n";
+ private static final String AWS_IDENTITY_DOC_COLON_SPLITTER = ":";
+
+ private static final URI AWS_INSTANCE_IDENTITY_DOCUMENT_URI =
+ URI.create("http://169.254.169.254/latest/dynamic/instance-identity/document");
+
+ @GuardedBy("monitor")
+ @javax.annotation.Nullable
+ private static Map<String, String> awsEnvVarMap = null;
+
+ // Detects if the application is running on EC2 by making a connection to AWS instance
+ // identity document URI. If connection is successful, application should be on an EC2 instance.
+ private static volatile boolean isRunningOnAwsEc2 = false;
+
+ static {
+ initializeAwsIdentityDocument();
+ }
+
+ static boolean isRunningOnAwsEc2() {
+ return isRunningOnAwsEc2;
+ }
+
+ // Tries to establish an HTTP connection to AWS instance identity document url. If the application
+ // is running on an EC2 instance, we should be able to get back a valid JSON document. Parses that
+ // document and stores the identity properties in a local map.
+ // This method should only be called once.
+ private static void initializeAwsIdentityDocument() {
+ InputStream stream = null;
+ try {
+ stream = openStream(AWS_INSTANCE_IDENTITY_DOCUMENT_URI);
+ String awsIdentityDocument = slurp(new InputStreamReader(stream, Charset.forName("UTF-8")));
+ synchronized (monitor) {
+ awsEnvVarMap = parseAwsIdentityDocument(awsIdentityDocument);
+ }
+ isRunningOnAwsEc2 = true;
+ } catch (IOException e) {
+ // Cannot connect to http://169.254.169.254/latest/dynamic/instance-identity/document.
+ // Not on an AWS EC2 instance.
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ // Do nothing.
+ }
+ }
+ }
+ }
+
+ /** quick http client that allows no-dependency try at getting instance data. */
+ private static InputStream openStream(URI uri) throws IOException {
+ HttpURLConnection connection = HttpURLConnection.class.cast(uri.toURL().openConnection());
+ connection.setConnectTimeout(1000 * 2);
+ connection.setReadTimeout(1000 * 2);
+ connection.setAllowUserInteraction(false);
+ connection.setInstanceFollowRedirects(false);
+ return connection.getInputStream();
+ }
+
+ /** returns the {@code reader} as a string without closing it. */
+ private static String slurp(Reader reader) throws IOException {
+ StringBuilder to = new StringBuilder();
+ CharBuffer buf = CharBuffer.allocate(AWS_IDENTITY_DOC_BUF_SIZE);
+ while (reader.read(buf) != -1) {
+ buf.flip();
+ to.append(buf);
+ buf.clear();
+ }
+ return to.toString();
+ }
+
+ // AWS Instance Identity Document is a JSON file.
+ // See docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html.
+ static Map<String, String> parseAwsIdentityDocument(String awsIdentityDocument) {
+ Map<String, String> map = new HashMap<String, String>();
+ @SuppressWarnings("StringSplitter")
+ String[] lines = awsIdentityDocument.split(AWS_IDENTITY_DOC_LINE_BREAK_SPLITTER, -1);
+ for (String line : lines) {
+ @SuppressWarnings("StringSplitter")
+ String[] keyValuePair = line.split(AWS_IDENTITY_DOC_COLON_SPLITTER, -1);
+ if (keyValuePair.length != 2) {
+ continue;
+ }
+ String key = keyValuePair[0].replaceAll("[\" ]", "");
+ String value = keyValuePair[1].replaceAll("[\" ,]", "");
+ map.put(key, value);
+ }
+ return map;
+ }
+
+ @javax.annotation.Nullable
+ static String getValueFromAwsIdentityDocument(String key) {
+ synchronized (monitor) {
+ if (awsEnvVarMap == null) {
+ return null;
+ }
+ return awsEnvVarMap.get(key);
+ }
+ }
+
+ private AwsIdentityDocUtils() {}
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/GcpMetadataConfig.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/GcpMetadataConfig.java
new file mode 100644
index 00000000..c09d1c65
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/GcpMetadataConfig.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018, 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.contrib.monitoredresource.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.Charset;
+import javax.annotation.Nullable;
+
+/**
+ * Retrieves Google Cloud project-id and a limited set of instance attributes from Metadata server.
+ *
+ * @see <a href="https://cloud.google.com/compute/docs/storing-retrieving-metadata">
+ * https://cloud.google.com/compute/docs/storing-retrieving-metadata</a>
+ */
+final class GcpMetadataConfig {
+
+ private static final String METADATA_URL = "http://metadata/computeMetadata/v1/";
+
+ private GcpMetadataConfig() {}
+
+ @Nullable
+ static String getProjectId() {
+ return getAttribute("project/project-id");
+ }
+
+ @Nullable
+ static String getZone() {
+ String zoneId = getAttribute("instance/zone");
+ if (zoneId == null) {
+ return null;
+ }
+ if (zoneId.contains("/")) {
+ return zoneId.substring(zoneId.lastIndexOf('/') + 1);
+ }
+ return zoneId;
+ }
+
+ @Nullable
+ static String getInstanceId() {
+ return getAttribute("instance/id");
+ }
+
+ @Nullable
+ static String getClusterName() {
+ return getAttribute("instance/attributes/cluster-name");
+ }
+
+ @Nullable
+ private static String getAttribute(String attributeName) {
+ try {
+ URL url = new URL(METADATA_URL + attributeName);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestProperty("Metadata-Flavor", "Google");
+ InputStream input = connection.getInputStream();
+ if (connection.getResponseCode() == 200) {
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new InputStreamReader(input, Charset.forName("UTF-8")));
+ return reader.readLine();
+ } finally {
+ if (reader != null) {
+ reader.close();
+ }
+ }
+ }
+ } catch (IOException ignore) {
+ // ignore
+ }
+ return null;
+ }
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResource.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResource.java
new file mode 100644
index 00000000..c828906d
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResource.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2018, 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.contrib.monitoredresource.util;
+
+import com.google.auto.value.AutoValue;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * {@link MonitoredResource} represents an auto-detected monitored resource used by application for
+ * exporting stats. It has a {@code ResourceType} associated with a mapping from resource labels to
+ * values.
+ *
+ * @since 0.13
+ */
+@Immutable
+public abstract class MonitoredResource {
+
+ MonitoredResource() {}
+
+ /**
+ * Returns the {@link ResourceType} of this {@link MonitoredResource}.
+ *
+ * @return the {@code ResourceType}.
+ * @since 0.13
+ */
+ public abstract ResourceType getResourceType();
+
+ /*
+ * Returns the first of two given parameters that is not null, if either is, or otherwise
+ * throws a NullPointerException.
+ */
+ private static <T> T firstNonNull(@Nullable T first, @Nullable T second) {
+ if (first != null) {
+ return first;
+ }
+ if (second != null) {
+ return second;
+ }
+ throw new NullPointerException("Both parameters are null");
+ }
+
+ // TODO(songya): consider using a tagged union match() approach (that will introduce
+ // dependency on opencensus-api).
+
+ /**
+ * {@link MonitoredResource} for AWS EC2 instance.
+ *
+ * @since 0.13
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class AwsEc2InstanceMonitoredResource extends MonitoredResource {
+
+ private static final String AWS_ACCOUNT =
+ firstNonNull(AwsIdentityDocUtils.getValueFromAwsIdentityDocument("accountId"), "");
+ private static final String AWS_INSTANCE_ID =
+ firstNonNull(AwsIdentityDocUtils.getValueFromAwsIdentityDocument("instanceId"), "");
+ private static final String AWS_REGION =
+ firstNonNull(AwsIdentityDocUtils.getValueFromAwsIdentityDocument("region"), "");
+
+ @Override
+ public ResourceType getResourceType() {
+ return ResourceType.AWS_EC2_INSTANCE;
+ }
+
+ /**
+ * Returns the AWS account ID.
+ *
+ * @return the AWS account ID.
+ * @since 0.13
+ */
+ public abstract String getAccount();
+
+ /**
+ * Returns the AWS EC2 instance ID.
+ *
+ * @return the AWS EC2 instance ID.
+ * @since 0.13
+ */
+ public abstract String getInstanceId();
+
+ /**
+ * Returns the AWS region.
+ *
+ * @return the AWS region.
+ * @since 0.13
+ */
+ public abstract String getRegion();
+
+ /**
+ * Returns an {@link AwsEc2InstanceMonitoredResource}.
+ *
+ * @param account the AWS account ID.
+ * @param instanceId the AWS EC2 instance ID.
+ * @param region the AWS region.
+ * @return an {@code AwsEc2InstanceMonitoredResource}.
+ * @since 0.15
+ */
+ public static AwsEc2InstanceMonitoredResource create(
+ String account, String instanceId, String region) {
+ return new AutoValue_MonitoredResource_AwsEc2InstanceMonitoredResource(
+ account, instanceId, region);
+ }
+
+ static AwsEc2InstanceMonitoredResource create() {
+ return create(AWS_ACCOUNT, AWS_INSTANCE_ID, AWS_REGION);
+ }
+ }
+
+ /**
+ * {@link MonitoredResource} for GCP GCE instance.
+ *
+ * @since 0.13
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class GcpGceInstanceMonitoredResource extends MonitoredResource {
+
+ private static final String GCP_ACCOUNT_ID = firstNonNull(GcpMetadataConfig.getProjectId(), "");
+ private static final String GCP_INSTANCE_ID =
+ firstNonNull(GcpMetadataConfig.getInstanceId(), "");
+ private static final String GCP_ZONE = firstNonNull(GcpMetadataConfig.getZone(), "");
+
+ @Override
+ public ResourceType getResourceType() {
+ return ResourceType.GCP_GCE_INSTANCE;
+ }
+
+ /**
+ * Returns the GCP account number for the instance.
+ *
+ * @return the GCP account number for the instance.
+ * @since 0.13
+ */
+ public abstract String getAccount();
+
+ /**
+ * Returns the GCP GCE instance ID.
+ *
+ * @return the GCP GCE instance ID.
+ * @since 0.13
+ */
+ public abstract String getInstanceId();
+
+ /**
+ * Returns the GCP zone.
+ *
+ * @return the GCP zone.
+ * @since 0.13
+ */
+ public abstract String getZone();
+
+ /**
+ * Returns a {@link GcpGceInstanceMonitoredResource}.
+ *
+ * @param account the GCP account number.
+ * @param instanceId the GCP GCE instance ID.
+ * @param zone the GCP zone.
+ * @return a {@code GcpGceInstanceMonitoredResource}.
+ * @since 0.15
+ */
+ public static GcpGceInstanceMonitoredResource create(
+ String account, String instanceId, String zone) {
+ return new AutoValue_MonitoredResource_GcpGceInstanceMonitoredResource(
+ account, instanceId, zone);
+ }
+
+ static GcpGceInstanceMonitoredResource create() {
+ return create(GCP_ACCOUNT_ID, GCP_INSTANCE_ID, GCP_ZONE);
+ }
+ }
+
+ /**
+ * {@link MonitoredResource} for GCP GKE container.
+ *
+ * @since 0.13
+ */
+ @Immutable
+ @AutoValue
+ public abstract static class GcpGkeContainerMonitoredResource extends MonitoredResource {
+
+ private static final String GCP_ACCOUNT_ID = firstNonNull(GcpMetadataConfig.getProjectId(), "");
+ private static final String GCP_CLUSTER_NAME =
+ firstNonNull(GcpMetadataConfig.getClusterName(), "");
+ private static final String GCP_CONTAINER_NAME =
+ firstNonNull(System.getenv("CONTAINER_NAME"), "");
+ private static final String GCP_NAMESPACE_ID = firstNonNull(System.getenv("NAMESPACE"), "");
+ private static final String GCP_INSTANCE_ID =
+ firstNonNull(GcpMetadataConfig.getInstanceId(), "");
+ private static final String GCP_POD_ID = firstNonNull(System.getenv("HOSTNAME"), "");
+ private static final String GCP_ZONE = firstNonNull(GcpMetadataConfig.getZone(), "");
+
+ @Override
+ public ResourceType getResourceType() {
+ return ResourceType.GCP_GKE_CONTAINER;
+ }
+
+ /**
+ * Returns the GCP account number for the instance.
+ *
+ * @return the GCP account number for the instance.
+ * @since 0.13
+ */
+ public abstract String getAccount();
+
+ /**
+ * Returns the GCP GKE cluster name.
+ *
+ * @return the GCP GKE cluster name.
+ * @since 0.13
+ */
+ public abstract String getClusterName();
+
+ /**
+ * Returns the GCP GKE container name.
+ *
+ * @return the GCP GKE container name.
+ * @since 0.13
+ */
+ public abstract String getContainerName();
+
+ /**
+ * Returns the GCP GKE namespace ID.
+ *
+ * @return the GCP GKE namespace ID.
+ * @since 0.13
+ */
+ public abstract String getNamespaceId();
+
+ /**
+ * Returns the GCP GKE instance ID.
+ *
+ * @return the GCP GKE instance ID.
+ * @since 0.13
+ */
+ public abstract String getInstanceId();
+
+ /**
+ * Returns the GCP GKE Pod ID.
+ *
+ * @return the GCP GKE Pod ID.
+ * @since 0.13
+ */
+ public abstract String getPodId();
+
+ /**
+ * Returns the GCP zone.
+ *
+ * @return the GCP zone.
+ * @since 0.13
+ */
+ public abstract String getZone();
+
+ /**
+ * Returns a {@link GcpGkeContainerMonitoredResource}.
+ *
+ * @param account the GCP account number.
+ * @param clusterName the GCP GKE cluster name.
+ * @param containerName the GCP GKE container name.
+ * @param namespaceId the GCP GKE namespace ID.
+ * @param instanceId the GCP GKE instance ID.
+ * @param podId the GCP GKE Pod ID.
+ * @param zone the GCP zone.
+ * @return a {@code GcpGkeContainerMonitoredResource}.
+ * @since 0.15
+ */
+ public static GcpGkeContainerMonitoredResource create(
+ String account,
+ String clusterName,
+ String containerName,
+ String namespaceId,
+ String instanceId,
+ String podId,
+ String zone) {
+ return new AutoValue_MonitoredResource_GcpGkeContainerMonitoredResource(
+ account, clusterName, containerName, namespaceId, instanceId, podId, zone);
+ }
+
+ static GcpGkeContainerMonitoredResource create() {
+ return create(
+ GCP_ACCOUNT_ID,
+ GCP_CLUSTER_NAME,
+ GCP_CONTAINER_NAME,
+ GCP_NAMESPACE_ID,
+ GCP_INSTANCE_ID,
+ GCP_POD_ID,
+ GCP_ZONE);
+ }
+ }
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtils.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtils.java
new file mode 100644
index 00000000..8ff0ff98
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.contrib.monitoredresource.util;
+
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import javax.annotation.Nullable;
+
+/**
+ * Utilities for for auto detecting monitored resource based on the environment where the
+ * application is running.
+ *
+ * @since 0.13
+ */
+public final class MonitoredResourceUtils {
+
+ /**
+ * Returns a self-configured monitored resource, or {@code null} if the application is not running
+ * on a supported environment.
+ *
+ * @return a {@code MonitoredResource}.
+ * @since 0.13
+ */
+ @Nullable
+ public static MonitoredResource getDefaultResource() {
+ if (System.getenv("KUBERNETES_SERVICE_HOST") != null) {
+ return GcpGkeContainerMonitoredResource.create();
+ }
+ if (GcpMetadataConfig.getInstanceId() != null) {
+ return GcpGceInstanceMonitoredResource.create();
+ }
+ if (AwsIdentityDocUtils.isRunningOnAwsEc2()) {
+ return AwsEc2InstanceMonitoredResource.create();
+ }
+ return null;
+ }
+
+ private MonitoredResourceUtils() {}
+}
diff --git a/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/ResourceType.java b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/ResourceType.java
new file mode 100644
index 00000000..f2816676
--- /dev/null
+++ b/contrib/monitored_resource_util/src/main/java/io/opencensus/contrib/monitoredresource/util/ResourceType.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018, 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.contrib.monitoredresource.util;
+
+/**
+ * {@link ResourceType} represents the type of supported monitored resources that can be
+ * automatically detected by OpenCensus.
+ *
+ * @since 0.13
+ */
+public enum ResourceType {
+
+ /**
+ * Resource for GCP GKE container.
+ *
+ * @since 0.13
+ */
+ GCP_GKE_CONTAINER,
+
+ /**
+ * Resource for GCP GCE instance.
+ *
+ * @since 0.13
+ */
+ GCP_GCE_INSTANCE,
+
+ /**
+ * Resource for AWS EC2 instance.
+ *
+ * @since 0.13
+ */
+ AWS_EC2_INSTANCE
+}
diff --git a/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtilsTest.java b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtilsTest.java
new file mode 100644
index 00000000..77d98493
--- /dev/null
+++ b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/AwsIdentityDocUtilsTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018, 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.contrib.monitoredresource.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link AwsIdentityDocUtils}. */
+@RunWith(JUnit4.class)
+public class AwsIdentityDocUtilsTest {
+
+ private static final String SAMPLE_AWS_IDENTITY_DOCUMENT =
+ "{\n"
+ + " \"devpayProductCodes\" : null,\n"
+ + " \"marketplaceProductCodes\" : [ \"1abc2defghijklm3nopqrs4tu\" ], \n"
+ + " \"availabilityZone\" : \"us-west-2b\",\n"
+ + " \"privateIp\" : \"10.158.112.84\",\n"
+ + " \"version\" : \"2017-09-30\",\n"
+ + " \"instanceId\" : \"i-1234567890abcdef0\",\n"
+ + " \"billingProducts\" : null,\n"
+ + " \"instanceType\" : \"t2.micro\",\n"
+ + " \"accountId\" : \"123456789012\",\n"
+ + " \"imageId\" : \"ami-5fb8c835\",\n"
+ + " \"pendingTime\" : \"2016-11-19T16:32:11Z\",\n"
+ + " \"architecture\" : \"x86_64\",\n"
+ + " \"kernelId\" : null,\n"
+ + " \"ramdiskId\" : null,\n"
+ + " \"region\" : \"us-west-2\"\n"
+ + "}";
+
+ @Test
+ public void testParseAwsIdentityDocument() {
+ Map<String, String> envVarMap =
+ AwsIdentityDocUtils.parseAwsIdentityDocument(SAMPLE_AWS_IDENTITY_DOCUMENT);
+ assertThat(envVarMap).containsEntry("instanceId", "i-1234567890abcdef0");
+ assertThat(envVarMap).containsEntry("accountId", "123456789012");
+ assertThat(envVarMap).containsEntry("region", "us-west-2");
+ }
+}
diff --git a/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceTest.java b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceTest.java
new file mode 100644
index 00000000..0defcbd7
--- /dev/null
+++ b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018, 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.contrib.monitoredresource.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MonitoredResource}. */
+@RunWith(JUnit4.class)
+public class MonitoredResourceTest {
+
+ private static final String AWS_ACCOUNT = "aws-account";
+ private static final String AWS_INSTANCE = "instance";
+ private static final String AWS_REGION = "us-west-2";
+ private static final String GCP_PROJECT = "gcp-project";
+ private static final String GCP_INSTANCE = "instance";
+ private static final String GCP_ZONE = "us-east1";
+ private static final String GCP_GKE_NAMESPACE = "namespace";
+ private static final String GCP_GKE_POD_ID = "pod-id";
+ private static final String GCP_GKE_CONTAINER_NAME = "container";
+ private static final String GCP_GKE_CLUSTER_NAME = "cluster";
+
+ @Test
+ public void testAwsEc2InstanceMonitoredResource() {
+ AwsEc2InstanceMonitoredResource resource =
+ AwsEc2InstanceMonitoredResource.create(AWS_ACCOUNT, AWS_INSTANCE, AWS_REGION);
+ assertThat(resource.getResourceType()).isEqualTo(ResourceType.AWS_EC2_INSTANCE);
+ assertThat(resource.getAccount()).isEqualTo(AWS_ACCOUNT);
+ assertThat(resource.getInstanceId()).isEqualTo(AWS_INSTANCE);
+ assertThat(resource.getRegion()).isEqualTo(AWS_REGION);
+ }
+
+ @Test
+ public void testGcpGceInstanceMonitoredResource() {
+ GcpGceInstanceMonitoredResource resource =
+ GcpGceInstanceMonitoredResource.create(GCP_PROJECT, GCP_INSTANCE, GCP_ZONE);
+ assertThat(resource.getResourceType()).isEqualTo(ResourceType.GCP_GCE_INSTANCE);
+ assertThat(resource.getAccount()).isEqualTo(GCP_PROJECT);
+ assertThat(resource.getInstanceId()).isEqualTo(GCP_INSTANCE);
+ assertThat(resource.getZone()).isEqualTo(GCP_ZONE);
+ }
+
+ @Test
+ public void testGcpGkeContainerMonitoredResource() {
+ GcpGkeContainerMonitoredResource resource =
+ GcpGkeContainerMonitoredResource.create(
+ GCP_PROJECT,
+ GCP_GKE_CLUSTER_NAME,
+ GCP_GKE_CONTAINER_NAME,
+ GCP_GKE_NAMESPACE,
+ GCP_INSTANCE,
+ GCP_GKE_POD_ID,
+ GCP_ZONE);
+ assertThat(resource.getResourceType()).isEqualTo(ResourceType.GCP_GKE_CONTAINER);
+ assertThat(resource.getAccount()).isEqualTo(GCP_PROJECT);
+ assertThat(resource.getClusterName()).isEqualTo(GCP_GKE_CLUSTER_NAME);
+ assertThat(resource.getContainerName()).isEqualTo(GCP_GKE_CONTAINER_NAME);
+ assertThat(resource.getNamespaceId()).isEqualTo(GCP_GKE_NAMESPACE);
+ assertThat(resource.getInstanceId()).isEqualTo(GCP_INSTANCE);
+ assertThat(resource.getPodId()).isEqualTo(GCP_GKE_POD_ID);
+ assertThat(resource.getZone()).isEqualTo(GCP_ZONE);
+ }
+}
diff --git a/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtilsTest.java b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtilsTest.java
new file mode 100644
index 00000000..01927a2d
--- /dev/null
+++ b/contrib/monitored_resource_util/src/test/java/io/opencensus/contrib/monitoredresource/util/MonitoredResourceUtilsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018, 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.contrib.monitoredresource.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MonitoredResourceUtils}. */
+@RunWith(JUnit4.class)
+public class MonitoredResourceUtilsTest {
+
+ @Test
+ public void testGetDefaultResource() {
+ MonitoredResource resource = MonitoredResourceUtils.getDefaultResource();
+ if (System.getenv("KUBERNETES_SERVICE_HOST") != null) {
+ assertThat(resource.getResourceType()).isEqualTo(ResourceType.GCP_GKE_CONTAINER);
+ } else if (GcpMetadataConfig.getInstanceId() != null) {
+ assertThat(resource.getResourceType()).isEqualTo(ResourceType.GCP_GCE_INSTANCE);
+ } else if (AwsIdentityDocUtils.isRunningOnAwsEc2()) {
+ assertThat(resource.getResourceType()).isEqualTo(ResourceType.AWS_EC2_INSTANCE);
+ } else {
+ assertThat(resource).isNull();
+ }
+ }
+}
diff --git a/contrib/spring/README.md b/contrib/spring/README.md
new file mode 100644
index 00000000..8c740297
--- /dev/null
+++ b/contrib/spring/README.md
@@ -0,0 +1,160 @@
+# spring
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+Provides annotation support for projects that use Spring.
+
+## Quickstart
+
+### Add the dependencies to your project.
+
+Replace `SPRING_VERSION` with the version of spring you're using.
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <!-- census -->
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-spring</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+
+ <!-- spring aspects -->
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-aspects</artifactId>
+ <version>SPRING_VERSION</version>
+ <scope>runtime</scope>
+ </dependency>
+
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-contrib-spring:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+runtime 'org.springframework:spring-aspects:SPRING_VERSION'
+```
+
+### Features
+
+#### Traced Annotation
+
+The `opencensus-contrib-spring` package provides support for a `@Traced` annotation
+that can be applied to methods. When applied, the method will be wrapped in a
+Span, [https://github.com/census-instrumentation/opencensus-specs/blob/master/trace/Span.md](https://github.com/census-instrumentation/opencensus-specs/blob/master/trace/Span.md)
+
+If the method throws an exception, the `Span` will be marked with a status of `Status.UNKNOWN`
+and the stack trace will be added to the span as an annotation.
+
+To enable the `@Traced` annotation, include the `CensusSpringAspect` bean.
+
+```xml
+ <!-- traces explicit calls to Traced -->
+ <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect">
+ <constructor-arg ref="tracer"/>
+ </bean>
+```
+
+#### Database Support
+
+The `opencensus-contrib-spring` package also includes support for tracing database
+calls. When database support is included, all calls to `java.sql.PreparedStatement.execute*`
+will be wrapped in a Span in the same way that `@Traced` wraps methods.
+
+To enable database support, include the `CensusSpringSqlAspect` bean.
+
+```xml
+ <!-- traces all SQL calls -->
+ <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSqlAspect">
+ <constructor-arg ref="tracer"/>
+ </bean>
+```
+
+#### Complete Spring XML configuration
+
+The following contains a complete spring xml file to configure `opencensus-contrib-spring`
+with support for both `@Traced` and database connection tracing.
+
+**Note:** This example does not include the configuration of any exporters. That will
+need to be done separately.
+
+**TBD:*** Include examples of spring with exporters.
+
+```xml
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:aop="http://www.springframework.org/schema/aop"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
+
+ <aop:aspectj-autoproxy/>
+
+ <!-- traces explicit calls to Traced -->
+ <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect">
+ <constructor-arg ref="tracer"/>
+ </bean>
+
+ <!-- traces all SQL calls -->
+ <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSqlAspect">
+ <constructor-arg ref="tracer"/>
+ </bean>
+
+ <!-- global tracer -->
+ <bean id="tracer" class="io.opencensus.trace.Tracing" factory-method="getTracer"/>
+</beans>
+```
+
+### Traced Usage
+
+Once configured, you can use the `@Traced` annotation to indicate that a method should
+be wrapped with a `Span`. By default, `@Traced` will use the name of the method as the
+span name. However, `@Traced` supports an optional name attribute to allow a custom
+span name to be specified.
+
+```java
+ @Traced()
+ void example1() {
+ // do work
+ }
+
+ // a custom span name can also be provided to Traced
+ @Traced(name = "custom-span-name")
+ void example2() {
+ // do moar work
+ }
+```
+
+#### Notes
+
+`opencensus-contrib-spring` support only enables annotations. You will still need to configure opencensus and register exporters / views.
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-spring/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-spring
+
+#### Java Versions
+
+Java 6 or above is required for using this artifact.
+
+#### About the `aop` package
+
+`opencensus-contrib-spring` makes heavy use of Aspect Oriented Programming [AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming) to
+add behavior to annotations. Fortunately, Spring supports this natively so we can leverage the capabilities they've already built in.
diff --git a/contrib/spring/build.gradle b/contrib/spring/build.gradle
new file mode 100644
index 00000000..941afcce
--- /dev/null
+++ b/contrib/spring/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Spring'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.spring_aspects,
+ libraries.spring_context
+
+ testCompile project(':opencensus-impl'),
+ project(':opencensus-testing'),
+ libraries.aspectj,
+ libraries.spring_test
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java
new file mode 100644
index 00000000..2edc57c7
--- /dev/null
+++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringAspect.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2018, 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.contrib.spring.aop;
+
+import io.opencensus.trace.Tracer;
+import java.lang.reflect.Method;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Configurable;
+
+/**
+ * CensusSpringAspect handles logic for the `@Traced` annotation.
+ *
+ * @since 0.16.0
+ */
+@Aspect
+@Configurable
+public final class CensusSpringAspect {
+ private final Tracer tracer;
+
+ /**
+ * Creates a {@code CensusSpringAspect} with the given tracer.
+ *
+ * @param tracer the tracer responsible for building new spans
+ * @since 0.16.0
+ */
+ public CensusSpringAspect(Tracer tracer) {
+ this.tracer = tracer;
+ }
+
+ /**
+ * trace handles methods executed with the `@Traced` annotation. A new span will be created with
+ * an optionally customizable span name.
+ *
+ * @param call the join point to execute
+ * @return the result of the invocation
+ * @throws Throwable if the underlying target throws an exception
+ * @since 0.16.0
+ */
+ @Around("@annotation(io.opencensus.contrib.spring.aop.Traced)")
+ public Object trace(ProceedingJoinPoint call) throws Throwable {
+ MethodSignature signature = (MethodSignature) call.getSignature();
+ Method method = signature.getMethod();
+
+ Traced annotation = method.getAnnotation(Traced.class);
+ if (annotation == null) {
+ return call.proceed();
+ }
+ String spanName = annotation.name();
+ if (spanName.isEmpty()) {
+ spanName = method.getName();
+ }
+
+ return Handler.proceed(call, tracer, spanName);
+ }
+}
diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSqlAspect.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSqlAspect.java
new file mode 100644
index 00000000..0fbd7159
--- /dev/null
+++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/CensusSpringSqlAspect.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018, 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.contrib.spring.aop;
+
+import io.opencensus.trace.Tracer;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.beans.factory.annotation.Configurable;
+
+/**
+ * CensusSpringSqlAspect captures span from all SQL invocations that utilize
+ * java.sql.Statement.execute*
+ *
+ * @since 0.16.0
+ */
+@Aspect
+@Configurable
+public final class CensusSpringSqlAspect {
+ private final Tracer tracer;
+
+ /**
+ * Creates a {@code CensusSpringSqlAspect} with the given tracer.
+ *
+ * @param tracer the tracer responsible for building new spans
+ * @since 0.16.0
+ */
+ public CensusSpringSqlAspect(Tracer tracer) {
+ this.tracer = tracer;
+ }
+
+ /**
+ * trace handles invocations of java.sql.Statement.execute*. A new span will be created whose name
+ * is (execute|executeQuery|executeQuery)-(hash of sql).
+ *
+ * @since 0.16.0
+ */
+ @Around("execute() || testing()")
+ public Object trace(ProceedingJoinPoint call) throws Throwable {
+ if (call.getArgs().length == 0 || call.getArgs()[0] == null) {
+ return call.proceed();
+ }
+
+ String sql = (String) call.getArgs()[0];
+ String spanName = makeSpanName(call, sql);
+
+ return Handler.proceed(call, tracer, spanName, sql);
+ }
+
+ /**
+ * execute creates spans around all invocations of Statement.execute*. The raw SQL will be stored
+ * in an annotation associated with the Span
+ */
+ @Pointcut("execution(public !void java.sql.Statement.execute*(java.lang.String))")
+ protected void execute() {}
+
+ @Pointcut("execution(public void Sample.execute*(java.lang.String))")
+ protected void testing() {}
+
+ private static String makeSpanName(ProceedingJoinPoint call, String sql) {
+ String hash = Integer.toHexString(hashCode(sql.toCharArray()));
+ return call.getSignature().getName() + "-" + hash;
+ }
+
+ private static int hashCode(char[] seq) {
+ if (seq == null) {
+ return 0;
+ }
+
+ int hash = 0;
+ for (char c : seq) {
+ hash = 31 * hash + c;
+ }
+ return hash;
+ }
+}
diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java
new file mode 100644
index 00000000..218854b1
--- /dev/null
+++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Handler.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018, 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.contrib.spring.aop;
+
+import io.opencensus.common.Scope;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import java.util.HashMap;
+import java.util.Map;
+import org.aspectj.lang.ProceedingJoinPoint;
+
+/** Handler defines common logic for wrapping a span around the specified JoinPoint. */
+final class Handler {
+ private Handler() {}
+
+ static Object proceed(
+ ProceedingJoinPoint call, Tracer tracer, String spanName, String... annotations)
+ throws Throwable {
+ Scope scope = tracer.spanBuilder(spanName).startScopedSpan();
+ try {
+ for (String annotation : annotations) {
+ tracer.getCurrentSpan().addAnnotation(annotation);
+ }
+
+ return call.proceed();
+
+ } catch (Throwable t) {
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ String message = t.getMessage();
+ attributes.put(
+ "message", AttributeValue.stringAttributeValue(message == null ? "null" : message));
+ attributes.put("type", AttributeValue.stringAttributeValue(t.getClass().toString()));
+
+ Span span = tracer.getCurrentSpan();
+ span.addAnnotation("error", attributes);
+ span.setStatus(Status.UNKNOWN);
+ throw t;
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Traced.java b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Traced.java
new file mode 100644
index 00000000..51f7311c
--- /dev/null
+++ b/contrib/spring/src/main/java/io/opencensus/contrib/spring/aop/Traced.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018, 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.contrib.spring.aop;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Traced specifies the annotated method should be included in the Trace.
+ *
+ * <p>By default, the name of the method will be used for the span name. However, the span name can
+ * be explicitly set via the name interface.
+ *
+ * @since 0.16.0
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Traced {
+
+ /**
+ * The optional custom span name.
+ *
+ * @return the optional custom span name; if not specified the method name will be used as the
+ * span name
+ */
+ String name() default "";
+}
diff --git a/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringAspectTest.java b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringAspectTest.java
new file mode 100644
index 00000000..3e4415cf
--- /dev/null
+++ b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/CensusSpringAspectTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2018, 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.contrib.spring.aop;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.testing.export.TestHandler;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+/**
+ * CensusSpringAspectTest verifies the weaving and application of the spring aop annotations.
+ *
+ * <p>Test Logic:
+ *
+ * <ol>
+ * <li>Configure a simple bean, Sample, via spring.xml
+ * <li>Include spring annotation support in spring.xml
+ * <li>Use spring to load the Sample bean which will weave the census aspects into the bean.
+ * <li>Use the TestHandler (defined in @Before and @After) to capture generated span.
+ * <li>In each test, we verify the pointcuts are applied correctly by inspecting the span captured
+ * in the TestHandler.
+ * </ol>
+ */
+@RunWith(JUnit4.class)
+public class CensusSpringAspectTest {
+ ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
+
+ private TestHandler handler;
+
+ @Before
+ public void setup() {
+ handler = new TestHandler();
+
+ SpanExporter exporter = Tracing.getExportComponent().getSpanExporter();
+ exporter.registerHandler("testing", handler);
+
+ TraceParams params =
+ Tracing.getTraceConfig()
+ .getActiveTraceParams()
+ .toBuilder()
+ .setSampler(Samplers.alwaysSample())
+ .build();
+ Tracing.getTraceConfig().updateActiveTraceParams(params);
+ }
+
+ @After
+ public void teardown() {
+ SpanExporter exporter = Tracing.getExportComponent().getSpanExporter();
+ exporter.unregisterHandler("testing");
+ }
+
+ @Test
+ public void tracedUsesMethodAsSpanName() throws Exception {
+ // When
+ Sample sample = (Sample) context.getBean("sample");
+ sample.call(100);
+
+ // Then
+ List<SpanData> data = handler.waitForExport(1);
+ assertThat(data).isNotNull();
+ assertThat(data.size()).isEqualTo(1);
+ assertThat(data.get(0).getName()).isEqualTo("call");
+ }
+
+ @Test
+ public void tracedAcceptsCustomSpanName() throws Exception {
+ // When
+ Sample sample = (Sample) context.getBean("sample");
+ sample.custom(100);
+
+ // Then
+ List<SpanData> data = handler.waitForExport(1);
+ assertThat(data).isNotNull();
+ assertThat(data.size()).isEqualTo(1);
+ assertThat(data.get(0).getName()).isEqualTo("blah");
+ }
+
+ @Test
+ public void handlesException() {
+ // When
+ Sample sample = (Sample) context.getBean("sample");
+ try {
+ sample.boom();
+ } catch (Exception ignored) {
+ // ok
+ }
+
+ // Then
+ List<SpanData> spanList = handler.waitForExport(1);
+ assertThat(spanList).isNotNull();
+ assertThat(spanList.size()).isEqualTo(1);
+
+ SpanData spanData = spanList.get(0);
+ assertThat(spanData.getName()).isEqualTo("boom");
+ assertThat(spanData.getStatus()).isEqualTo(Status.UNKNOWN);
+
+ SpanData.TimedEvents<Annotation> annotations = spanData.getAnnotations();
+ assertThat(annotations).isNotNull();
+
+ List<SpanData.TimedEvent<Annotation>> events = annotations.getEvents();
+ assertThat(events.size()).isEqualTo(1);
+ assertThat(events.get(0).getEvent().getDescription()).isEqualTo("error");
+ }
+
+ @Test
+ public void sqlExecute() throws Exception {
+ // When
+ String sql = "select 1";
+ Sample sample = (Sample) context.getBean("sample");
+ sample.execute(sql);
+
+ // Then
+ List<SpanData> data = handler.waitForExport(1);
+ assertThat(data).isNotNull();
+ assertThat(data.size()).isEqualTo(1);
+ assertThat(data.get(0).getName()).isEqualTo("execute-4705ea0d"); // sql-{hash of sql statement}
+
+ List<SpanData.TimedEvent<Annotation>> events = data.get(0).getAnnotations().getEvents();
+ assertThat(events.size()).isEqualTo(1);
+ assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql);
+ }
+
+ @Test
+ public void sqlQuery() throws Exception {
+ // When
+ String sql = "select 2";
+ Sample sample = (Sample) context.getBean("sample");
+ sample.executeQuery(sql);
+
+ // Then
+ List<SpanData> data = handler.waitForExport(1);
+ assertThat(data).isNotNull();
+ assertThat(data.size()).isEqualTo(1);
+ assertThat(data.get(0).getName()).isEqualTo("executeQuery-4705ea0e");
+
+ SpanData.TimedEvents<Annotation> annotations = data.get(0).getAnnotations();
+ List<SpanData.TimedEvent<Annotation>> events = annotations.getEvents();
+ assertThat(events.size()).isEqualTo(1);
+ assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql);
+ }
+
+ @Test
+ public void sqlUpdate() throws Exception {
+ // When
+ String sql = "update content set value = 1";
+ Sample sample = (Sample) context.getBean("sample");
+ sample.executeUpdate(sql);
+
+ // Then
+ List<SpanData> data = handler.waitForExport(1);
+ assertThat(data).isNotNull();
+ assertThat(data.size()).isEqualTo(1);
+ assertThat(data.get(0).getName()).isEqualTo("executeUpdate-acaeb423");
+
+ List<SpanData.TimedEvent<Annotation>> events = data.get(0).getAnnotations().getEvents();
+ assertThat(events.size()).isEqualTo(1);
+ assertThat(events.get(0).getEvent().getDescription()).isEqualTo(sql);
+ }
+}
diff --git a/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java
new file mode 100644
index 00000000..87cb94fd
--- /dev/null
+++ b/contrib/spring/src/test/java/io/opencensus/contrib/spring/aop/Sample.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.contrib.spring.aop;
+
+import java.sql.SQLException;
+
+public class Sample {
+ @Traced()
+ void example1() {
+ // do work
+ }
+
+ @Traced(name = "custom-span-name")
+ void example2() {
+ // do moar work
+ }
+
+ @Traced()
+ void call(long delay) throws Exception {
+ Thread.sleep(delay);
+ }
+
+ @Traced(name = "blah")
+ void custom(long delay) throws Exception {
+ Thread.sleep(delay);
+ }
+
+ @Traced()
+ void boom() throws Exception {
+ throw new Exception("boom");
+ }
+
+ public void execute(String sql) throws SQLException {}
+
+ public void executeQuery(String sql) throws SQLException {}
+
+ public void executeUpdate(String sql) throws SQLException {}
+
+ public void executeLargeUpdate(String sql) throws SQLException {}
+}
diff --git a/contrib/spring/src/test/resources/spring.xml b/contrib/spring/src/test/resources/spring.xml
new file mode 100644
index 00000000..729c2e6c
--- /dev/null
+++ b/contrib/spring/src/test/resources/spring.xml
@@ -0,0 +1,23 @@
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:aop="http://www.springframework.org/schema/aop"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
+
+ <!-- register the bean we'll use for testing -->
+ <bean id="sample" class="io.opencensus.contrib.spring.aop.Sample"/>
+
+ <aop:aspectj-autoproxy/>
+
+ <!-- traces explicit calls to @Traced -->
+ <bean id="censusAspect" class="io.opencensus.contrib.spring.aop.CensusSpringAspect">
+ <constructor-arg ref="tracer"/>
+ </bean>
+
+ <!-- traces all SQL calls -->
+ <bean id="censusSQLAspect" class="io.opencensus.contrib.spring.aop.CensusSpringSqlAspect">
+ <constructor-arg ref="tracer"/>
+ </bean>
+
+ <!-- global tracer -->
+ <bean id="tracer" class="io.opencensus.trace.Tracing" factory-method="getTracer"/>
+</beans>
diff --git a/contrib/spring_sleuth_v1x/README.md b/contrib/spring_sleuth_v1x/README.md
new file mode 100644
index 00000000..3345783b
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/README.md
@@ -0,0 +1,52 @@
+# OpenCensus Spring Sleuth
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Spring Sleuth for Java* is a library for automatically
+propagating the OpenCensus trace context when working with [Spring Sleuth][spring-sleuth-url].
+
+This is an __experimental component__, please bring feedback to
+https://gitter.im/census-instrumentation/Lobby not the usual
+sleuth channel https://gitter.im/spring-cloud/spring-cloud-sleuth.
+
+This version is compatible with [Spring Boot 1.5.x][spring-boot-1.5-url].
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-spring-sleuth</artifactId>
+ <version>0.16.1</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-build</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.springframework.cloud</groupId>
+ <artifactId>spring-cloud-starter-sleuth</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-contrib-spring-sleuth:0.16.1'
+```
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-spring-sleuth/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-spring-sleuth
+[spring-boot-1.5-url]: https://github.com/spring-projects/spring-boot/tree/1.5.x
+[spring-sleuth-url]: https://github.com/spring-cloud/spring-cloud-sleuth
diff --git a/contrib/spring_sleuth_v1x/build.gradle b/contrib/spring_sleuth_v1x/build.gradle
new file mode 100644
index 00000000..53ff1c04
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Spring Sleuth'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.spring_boot_starter_web,
+ libraries.spring_cloud_build,
+ libraries.spring_cloud_starter_sleuth
+
+ testCompile project(':opencensus-impl'),
+ project(':opencensus-testing'),
+ libraries.spring_test
+
+ signature "org.codehaus.mojo.signature:java16:+@signature"
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthAutoConfiguration.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthAutoConfiguration.java
new file mode 100644
index 00000000..de4201fe
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthAutoConfiguration.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2018, 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.contrib.spring.sleuth.v1x;
+
+import io.opencensus.common.ExperimentalApi;
+import java.util.Random;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.sleuth.Sampler;
+import org.springframework.cloud.sleuth.SpanNamer;
+import org.springframework.cloud.sleuth.SpanReporter;
+import org.springframework.cloud.sleuth.TraceKeys;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration;
+import org.springframework.cloud.sleuth.log.SpanLogger;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.annotation.Role;
+
+/**
+ * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} that
+ * allows inter-operation between Sleuth(Brave) and OpenCensus.
+ *
+ * @since 0.16
+ */
+@ExperimentalApi
+@Configuration
+@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+@ConditionalOnProperty(name = "spring.opencensus.sleuth.enabled", matchIfMissing = true)
+@AutoConfigureBefore(TraceAutoConfiguration.class)
+@EnableConfigurationProperties(OpenCensusSleuthProperties.class)
+public class OpenCensusSleuthAutoConfiguration {
+
+ @Bean
+ @Primary
+ Tracer openCensusSleuthTracer(
+ Sampler sampler,
+ Random random,
+ SpanNamer spanNamer,
+ SpanLogger spanLogger,
+ SpanReporter spanReporter,
+ TraceKeys traceKeys) {
+ return new OpenCensusSleuthTracer(
+ sampler, random, spanNamer, spanLogger, spanReporter, traceKeys, /* traceId128= */ true);
+ }
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthProperties.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthProperties.java
new file mode 100644
index 00000000..5cd0e57c
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthProperties.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018, 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.contrib.spring.sleuth.v1x;
+
+import io.opencensus.common.ExperimentalApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Sleuth annotation settings.
+ *
+ * @since 0.16
+ */
+@ExperimentalApi
+@ConfigurationProperties("spring.opencensus.sleuth")
+public class OpenCensusSleuthProperties {
+
+ private boolean enabled = true;
+
+ /** Returns whether OpenCensus trace propagation is enabled. */
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ /** Enables OpenCensus trace propagation. */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpan.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpan.java
new file mode 100644
index 00000000..eeacfcb7
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpan.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2018, 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.contrib.spring.sleuth.v1x;
+
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import java.nio.ByteBuffer;
+import java.util.EnumSet;
+import java.util.Map;
+
+/**
+ * Implementaion of Span that is created from a Sleuth Span.
+ *
+ * @since 0.16
+ */
+@ExperimentalApi
+public class OpenCensusSleuthSpan extends Span {
+
+ private static final EnumSet<Options> recordOptions = EnumSet.of(Options.RECORD_EVENTS);
+ private static final EnumSet<Options> notRecordOptions = EnumSet.noneOf(Options.class);
+
+ private static final TraceOptions sampledOptions =
+ TraceOptions.builder().setIsSampled(true).build();
+ private static final TraceOptions notSampledOptions =
+ TraceOptions.builder().setIsSampled(false).build();
+
+ OpenCensusSleuthSpan(org.springframework.cloud.sleuth.Span span) {
+ super(
+ fromSleuthSpan(span),
+ Boolean.TRUE.equals(span.isExportable()) ? recordOptions : notRecordOptions);
+ }
+
+ @Override
+ public void addAnnotation(String s, Map<String, AttributeValue> map) {}
+
+ @Override
+ public void addAnnotation(Annotation annotation) {}
+
+ @Override
+ public void addLink(Link link) {}
+
+ @Override
+ public void end(EndSpanOptions endSpanOptions) {}
+
+ // TODO: upgrade to new SpanContext.create() once it has been released.
+ @SuppressWarnings("deprecation")
+ private static SpanContext fromSleuthSpan(org.springframework.cloud.sleuth.Span span) {
+ return SpanContext.create(
+ TraceId.fromBytes(
+ ByteBuffer.allocate(TraceId.SIZE)
+ .putLong(span.getTraceIdHigh())
+ .putLong(span.getTraceId())
+ .array()),
+ SpanId.fromBytes(ByteBuffer.allocate(SpanId.SIZE).putLong(span.getSpanId()).array()),
+ Boolean.TRUE.equals(span.isExportable()) ? sampledOptions : notSampledOptions);
+ }
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolder.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolder.java
new file mode 100644
index 00000000..db6a3555
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolder.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018, 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.contrib.spring.sleuth.v1x;
+
+import io.grpc.Context;
+import io.opencensus.common.ExperimentalApi;
+import io.opencensus.trace.unsafe.ContextUtils;
+import org.apache.commons.logging.Log;
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.core.NamedThreadLocal;
+
+/*>>>
+ import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Inspired by the Sleuth's {@code SpanContextHolder}. */
+@ExperimentalApi
+final class OpenCensusSleuthSpanContextHolder {
+ private static final Log log =
+ org.apache.commons.logging.LogFactory.getLog(OpenCensusSleuthSpanContextHolder.class);
+ private static final ThreadLocal</*@Nullable*/ SpanContext> CURRENT_SPAN =
+ new NamedThreadLocal</*@Nullable*/ SpanContext>("Trace Context");
+
+ // Get the current span out of the thread context.
+ @javax.annotation.Nullable
+ static Span getCurrentSpan() {
+ SpanContext currentSpanContext = CURRENT_SPAN.get();
+ return currentSpanContext != null ? currentSpanContext.span : null;
+ }
+
+ // Set the current span in the thread context
+ static void setCurrentSpan(Span span) {
+ if (log.isTraceEnabled()) {
+ log.trace("Setting current span " + span);
+ }
+ push(span, /* autoClose= */ false);
+ }
+
+ // Remove all thread context relating to spans (useful for testing).
+ // See close() for a better alternative in instrumetation
+ static void removeCurrentSpan() {
+ removeCurrentSpanInternal(null);
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ @javax.annotation.Nullable
+ private static SpanContext removeCurrentSpanInternal(
+ @javax.annotation.Nullable SpanContext toRestore) {
+ if (toRestore != null) {
+ setSpanContextInternal(toRestore);
+ } else {
+ CURRENT_SPAN.remove();
+ // This is a big hack and can cause other data in the io.grpc.Context to be lost. But
+ // Spring 1.5 does not use io.grpc.Context and because the framework does not accept any
+ // gRPC context, the context will always be ROOT anyway.
+ Context.ROOT.attach();
+ }
+ return toRestore;
+ }
+
+ // Check if there is already a span in the current thread.
+ static boolean isTracing() {
+ return CURRENT_SPAN.get() != null;
+ }
+
+ // Close the current span and all parents that can be auto closed. On every iteration a function
+ // will be applied on the closed Span.
+ static void close(SpanFunction spanFunction) {
+ SpanContext current = CURRENT_SPAN.get();
+ while (current != null) {
+ spanFunction.apply(current.span);
+ current = removeCurrentSpanInternal(current.parent);
+ if (current == null || !current.autoClose) {
+ return;
+ }
+ }
+ }
+
+ // Close the current span and all parents that can be auto closed.
+ static void close() {
+ close(NO_OP_FUNCTION);
+ }
+
+ /**
+ * Push a span into the thread context, with the option to have it auto close if any child spans
+ * are themselves closed. Use autoClose=true if you start a new span with a parent that wasn't
+ * already in thread context.
+ */
+ static void push(Span span, boolean autoClose) {
+ if (isCurrent(span)) {
+ return;
+ }
+ setSpanContextInternal(new SpanContext(span, autoClose));
+ }
+
+ interface SpanFunction {
+ void apply(Span span);
+ }
+
+ private static final SpanFunction NO_OP_FUNCTION =
+ new SpanFunction() {
+ @Override
+ public void apply(Span span) {}
+ };
+
+ @SuppressWarnings("CheckReturnValue")
+ private static void setSpanContextInternal(SpanContext spanContext) {
+ CURRENT_SPAN.set(spanContext);
+ spanContext.ocCurrentContext.attach();
+ }
+
+ private static boolean isCurrent(Span span) {
+ if (span == null) {
+ return false;
+ }
+ SpanContext currentSpanContext = CURRENT_SPAN.get();
+ return currentSpanContext != null && span.equals(currentSpanContext.span);
+ }
+
+ private static class SpanContext {
+ final Span span;
+ final boolean autoClose;
+ @javax.annotation.Nullable final SpanContext parent;
+ final OpenCensusSleuthSpan ocSpan;
+ final Context ocCurrentContext;
+
+ private SpanContext(Span span, boolean autoClose) {
+ this.span = span;
+ this.autoClose = autoClose;
+ this.parent = CURRENT_SPAN.get();
+ this.ocSpan = new OpenCensusSleuthSpan(span);
+ this.ocCurrentContext =
+ Context.current().withValue(ContextUtils.CONTEXT_SPAN_KEY, this.ocSpan);
+ }
+ }
+
+ private OpenCensusSleuthSpanContextHolder() {}
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracer.java b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracer.java
new file mode 100644
index 00000000..bba9bab2
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracer.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2018, 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.contrib.spring.sleuth.v1x;
+
+import io.opencensus.common.ExperimentalApi;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.cloud.sleuth.Sampler;
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.cloud.sleuth.SpanNamer;
+import org.springframework.cloud.sleuth.SpanReporter;
+import org.springframework.cloud.sleuth.TraceKeys;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.cloud.sleuth.instrument.async.SpanContinuingTraceCallable;
+import org.springframework.cloud.sleuth.instrument.async.SpanContinuingTraceRunnable;
+import org.springframework.cloud.sleuth.log.SpanLogger;
+import org.springframework.cloud.sleuth.util.ExceptionUtils;
+import org.springframework.cloud.sleuth.util.SpanNameUtil;
+
+/*>>>
+ import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Sleuth Tracer that keeps a synchronized OpenCensus Span. This class is based on Sleuth's {@code
+ * DefaultTracer}.
+ *
+ * @since 0.16
+ */
+@ExperimentalApi
+public class OpenCensusSleuthTracer implements Tracer {
+ private static final Log log = LogFactory.getLog(OpenCensusSleuthTracer.class);
+ private final Sampler defaultSampler;
+ private final Random random;
+ private final SpanNamer spanNamer;
+ private final SpanLogger spanLogger;
+ private final SpanReporter spanReporter;
+ private final TraceKeys traceKeys;
+ private final boolean traceId128;
+
+ /** Basic constructor holding components for implementing Sleuth's {@link Tracer} interface. */
+ public OpenCensusSleuthTracer(
+ Sampler defaultSampler,
+ Random random,
+ SpanNamer spanNamer,
+ SpanLogger spanLogger,
+ SpanReporter spanReporter,
+ TraceKeys traceKeys) {
+ this(
+ defaultSampler,
+ random,
+ spanNamer,
+ spanLogger,
+ spanReporter,
+ traceKeys,
+ /* traceId128= */ false);
+ }
+
+ /** Basic constructor holding components for implementing Sleuth's {@link Tracer} interface. */
+ public OpenCensusSleuthTracer(
+ Sampler defaultSampler,
+ Random random,
+ SpanNamer spanNamer,
+ SpanLogger spanLogger,
+ SpanReporter spanReporter,
+ TraceKeys traceKeys,
+ boolean traceId128) {
+ this.defaultSampler = defaultSampler;
+ this.random = random;
+ this.spanNamer = spanNamer;
+ this.spanLogger = spanLogger;
+ this.spanReporter = spanReporter;
+ this.traceId128 = traceId128;
+ this.traceKeys = traceKeys != null ? traceKeys : new TraceKeys();
+ }
+
+ @Override
+ @javax.annotation.Nullable
+ public Span createSpan(String name, /*@Nullable*/ Span parent) {
+ if (parent == null) {
+ return createSpan(name);
+ }
+ return continueSpan(createChild(parent, name));
+ }
+
+ @Override
+ @javax.annotation.Nullable
+ public Span createSpan(String name) {
+ return this.createSpan(name, this.defaultSampler);
+ }
+
+ @Override
+ @javax.annotation.Nullable
+ public Span createSpan(String name, /*@Nullable*/ Sampler sampler) {
+ String shortenedName = SpanNameUtil.shorten(name);
+ Span span;
+ if (isTracing()) {
+ span = createChild(getCurrentSpan(), shortenedName);
+ } else {
+ long id = createId();
+ span =
+ Span.builder()
+ .name(shortenedName)
+ .traceIdHigh(this.traceId128 ? createTraceIdHigh() : 0L)
+ .traceId(id)
+ .spanId(id)
+ .build();
+ if (sampler == null) {
+ sampler = this.defaultSampler;
+ }
+ span = sampledSpan(span, sampler);
+ this.spanLogger.logStartedSpan(null, span);
+ }
+ return continueSpan(span);
+ }
+
+ @Override
+ @javax.annotation.Nullable
+ public Span detach(/*@Nullable*/ Span span) {
+ if (span == null) {
+ return null;
+ }
+ Span current = OpenCensusSleuthSpanContextHolder.getCurrentSpan();
+ if (current == null) {
+ if (log.isTraceEnabled()) {
+ log.trace(
+ "Span in the context is null so something has already detached the span. "
+ + "Won't do anything about it");
+ }
+ return null;
+ }
+ if (!span.equals(current)) {
+ ExceptionUtils.warn(
+ "Tried to detach trace span but "
+ + "it is not the current span: "
+ + span
+ + ". You may have forgotten to close or detach "
+ + current);
+ } else {
+ OpenCensusSleuthSpanContextHolder.removeCurrentSpan();
+ }
+ return span.getSavedSpan();
+ }
+
+ @Override
+ @javax.annotation.Nullable
+ public Span close(/*@Nullable*/ Span span) {
+ if (span == null) {
+ return null;
+ }
+ final Span savedSpan = span.getSavedSpan();
+ Span current = OpenCensusSleuthSpanContextHolder.getCurrentSpan();
+ if (current == null || !span.equals(current)) {
+ ExceptionUtils.warn(
+ "Tried to close span but it is not the current span: "
+ + span
+ + ". You may have forgotten to close or detach "
+ + current);
+ } else {
+ span.stop();
+ if (savedSpan != null && span.getParents().contains(savedSpan.getSpanId())) {
+ this.spanReporter.report(span);
+ this.spanLogger.logStoppedSpan(savedSpan, span);
+ } else {
+ if (!span.isRemote()) {
+ this.spanReporter.report(span);
+ this.spanLogger.logStoppedSpan(null, span);
+ }
+ }
+ OpenCensusSleuthSpanContextHolder.close(
+ new OpenCensusSleuthSpanContextHolder.SpanFunction() {
+ @Override
+ public void apply(Span closedSpan) {
+ // Note: hasn't this already been done?
+ OpenCensusSleuthTracer.this.spanLogger.logStoppedSpan(savedSpan, closedSpan);
+ }
+ });
+ }
+ return savedSpan;
+ }
+
+ Span createChild(/*@Nullable*/ Span parent, String name) {
+ String shortenedName = SpanNameUtil.shorten(name);
+ long id = createId();
+ if (parent == null) {
+ Span span =
+ Span.builder()
+ .name(shortenedName)
+ .traceIdHigh(this.traceId128 ? createTraceIdHigh() : 0L)
+ .traceId(id)
+ .spanId(id)
+ .build();
+ span = sampledSpan(span, this.defaultSampler);
+ this.spanLogger.logStartedSpan(null, span);
+ return span;
+ } else {
+ if (!isTracing()) {
+ OpenCensusSleuthSpanContextHolder.push(parent, /* autoClose= */ true);
+ }
+ Span span =
+ Span.builder()
+ .name(shortenedName)
+ .traceIdHigh(parent.getTraceIdHigh())
+ .traceId(parent.getTraceId())
+ .parent(parent.getSpanId())
+ .spanId(id)
+ .processId(parent.getProcessId())
+ .savedSpan(parent)
+ .exportable(parent.isExportable())
+ .baggage(parent.getBaggage())
+ .build();
+ this.spanLogger.logStartedSpan(parent, span);
+ return span;
+ }
+ }
+
+ private static Span sampledSpan(Span span, Sampler sampler) {
+ if (!sampler.isSampled(span)) {
+ // Copy everything, except set exportable to false
+ return Span.builder()
+ .begin(span.getBegin())
+ .traceIdHigh(span.getTraceIdHigh())
+ .traceId(span.getTraceId())
+ .spanId(span.getSpanId())
+ .name(span.getName())
+ .exportable(false)
+ .build();
+ }
+ return span;
+ }
+
+ // Encodes a timestamp into the upper 32-bits, so that it can be converted to an Amazon trace ID.
+ // For example, an Amazon trace ID is composed of the following:
+ // |-- 32 bits for epoch seconds -- | -- 96 bits for random data -- |
+ //
+ // To support this, Span#getTraceIdHigh() holds the epoch seconds and first 32 random bits: and
+ // Span#getTraceId() holds the remaining 64 random bits.
+ private long createTraceIdHigh() {
+ long epochSeconds = System.currentTimeMillis() / 1000;
+ int random = this.random.nextInt();
+ return (epochSeconds & 0xffffffffL) << 32 | (random & 0xffffffffL);
+ }
+
+ private long createId() {
+ return this.random.nextLong();
+ }
+
+ @Override
+ @javax.annotation.Nullable
+ public Span continueSpan(/*@Nullable*/ Span span) {
+ if (span != null) {
+ this.spanLogger.logContinuedSpan(span);
+ } else {
+ return null;
+ }
+ Span newSpan = createContinuedSpan(span, OpenCensusSleuthSpanContextHolder.getCurrentSpan());
+ OpenCensusSleuthSpanContextHolder.setCurrentSpan(newSpan);
+ return newSpan;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static Span createContinuedSpan(Span span, /*@Nullable*/ Span saved) {
+ if (saved == null && span.getSavedSpan() != null) {
+ saved = span.getSavedSpan();
+ }
+ return new Span(span, saved);
+ }
+
+ @Override
+ @javax.annotation.Nullable
+ public Span getCurrentSpan() {
+ return OpenCensusSleuthSpanContextHolder.getCurrentSpan();
+ }
+
+ @Override
+ public boolean isTracing() {
+ return OpenCensusSleuthSpanContextHolder.isTracing();
+ }
+
+ @Override
+ public void addTag(String key, String value) {
+ Span s = getCurrentSpan();
+ if (s != null && s.isExportable()) {
+ s.tag(key, value);
+ }
+ }
+
+ /**
+ * Wrap the callable in a TraceCallable, if tracing.
+ *
+ * @return The callable provided, wrapped if tracing, 'callable' if not.
+ */
+ @Override
+ public <V> Callable<V> wrap(Callable<V> callable) {
+ if (isTracing()) {
+ return new SpanContinuingTraceCallable<V>(this, this.traceKeys, this.spanNamer, callable);
+ }
+ return callable;
+ }
+
+ /**
+ * Wrap the runnable in a TraceRunnable, if tracing.
+ *
+ * @return The runnable provided, wrapped if tracing, 'runnable' if not.
+ */
+ @Override
+ public Runnable wrap(Runnable runnable) {
+ if (isTracing()) {
+ return new SpanContinuingTraceRunnable(this, this.traceKeys, this.spanNamer, runnable);
+ }
+ return runnable;
+ }
+}
diff --git a/contrib/spring_sleuth_v1x/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/contrib/spring_sleuth_v1x/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/resources/META-INF/additional-spring-configuration-metadata.json
diff --git a/contrib/spring_sleuth_v1x/src/main/resources/META-INF/spring.factories b/contrib/spring_sleuth_v1x/src/main/resources/META-INF/spring.factories
new file mode 100644
index 00000000..5e654514
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,7 @@
+# Auto Configuration
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+io.opencensus.contrib.spring.sleuth.v1x.OpenCensusSleuthAutoConfiguration\
+
+# Environment Post Processor
+org.springframework.boot.env.EnvironmentPostProcessor=\
+org.springframework.cloud.sleuth.autoconfig.TraceEnvironmentPostProcessor
diff --git a/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolderTest.java b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolderTest.java
new file mode 100644
index 00000000..997ed4f4
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanContextHolderTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018, 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.contrib.spring.sleuth.v1x;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link OpenCensusSleuthSpanContextHolder}. */
+@RunWith(JUnit4.class)
+public class OpenCensusSleuthSpanContextHolderTest {
+ private static final Tracer tracer = Tracing.getTracer();
+
+ @After
+ @Before
+ public void verifyNotTracing() {
+ assertThat(OpenCensusSleuthSpanContextHolder.isTracing()).isFalse();
+ assertThat(tracer.getCurrentSpan().getContext().isValid()).isFalse();
+ }
+
+ @Test
+ public void testFromSleuthSampled() {
+ org.springframework.cloud.sleuth.Span sleuthSpan =
+ createSleuthSpan(21, 22, 23, /* exportable= */ true);
+ OpenCensusSleuthSpanContextHolder.setCurrentSpan(sleuthSpan);
+ assertThat(OpenCensusSleuthSpanContextHolder.isTracing()).isTrue();
+ assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpan);
+ assertSpanEquals(tracer.getCurrentSpan(), sleuthSpan);
+ assertThat(tracer.getCurrentSpan().getContext().getTraceOptions().isSampled()).isTrue();
+ OpenCensusSleuthSpanContextHolder.close();
+ }
+
+ @Test
+ public void testFromSleuthUnsampled() {
+ org.springframework.cloud.sleuth.Span sleuthSpan =
+ createSleuthSpan(21, 22, 23, /* exportable= */ false);
+ OpenCensusSleuthSpanContextHolder.setCurrentSpan(sleuthSpan);
+ assertThat(OpenCensusSleuthSpanContextHolder.isTracing()).isTrue();
+ assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpan);
+ assertSpanEquals(tracer.getCurrentSpan(), sleuthSpan);
+ assertThat(tracer.getCurrentSpan().getContext().getTraceOptions().isSampled()).isFalse();
+ OpenCensusSleuthSpanContextHolder.close();
+ }
+
+ @Test
+ public void testSpanStackSimple() {
+ org.springframework.cloud.sleuth.Span[] sleuthSpans = createSleuthSpans(4);
+ // push all the spans
+ for (int i = 0; i < sleuthSpans.length; i++) {
+ OpenCensusSleuthSpanContextHolder.push(sleuthSpans[i], /* autoClose= */ false);
+ assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpans[i]);
+ assertSpanEquals(tracer.getCurrentSpan(), sleuthSpans[i]);
+ }
+ // pop all the spans
+ for (int i = sleuthSpans.length - 1; i >= 0; i--) {
+ assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpans[i]);
+ assertSpanEquals(tracer.getCurrentSpan(), sleuthSpans[i]);
+ OpenCensusSleuthSpanContextHolder.close();
+ }
+ }
+
+ @Test
+ public void testSpanStackAutoClose() {
+ org.springframework.cloud.sleuth.Span[] sleuthSpans = createSleuthSpans(4);
+ // push all the spans
+ for (int i = 0; i < sleuthSpans.length; i++) {
+ // set autoclose for all the spans except 2
+ OpenCensusSleuthSpanContextHolder.push(sleuthSpans[i], /* autoClose= */ i != 2);
+ assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpans[i]);
+ assertSpanEquals(tracer.getCurrentSpan(), sleuthSpans[i]);
+ }
+ // verify autoClose pops stack to index 2
+ OpenCensusSleuthSpanContextHolder.close();
+ assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(sleuthSpans[2]);
+ assertSpanEquals(tracer.getCurrentSpan(), sleuthSpans[2]);
+ // verify autoClose closes pops rest of stack
+ OpenCensusSleuthSpanContextHolder.close();
+ }
+
+ @Test
+ public void testSpanStackCloseSpanFunction() {
+ final org.springframework.cloud.sleuth.Span[] sleuthSpans = createSleuthSpans(4);
+ // push all the spans
+ for (int i = 0; i < sleuthSpans.length; i++) {
+ OpenCensusSleuthSpanContextHolder.push(sleuthSpans[i], /* autoClose= */ false);
+ }
+ // pop all the spans, verify that given SpanFunction is called on the closed span.
+ for (int i = sleuthSpans.length - 1; i >= 0; i--) {
+ final int index = i;
+ OpenCensusSleuthSpanContextHolder.close(
+ new OpenCensusSleuthSpanContextHolder.SpanFunction() {
+ @Override
+ public void apply(org.springframework.cloud.sleuth.Span span) {
+ assertThat(span).isEqualTo(sleuthSpans[index]);
+ }
+ });
+ }
+ }
+
+ org.springframework.cloud.sleuth.Span[] createSleuthSpans(int len) {
+ org.springframework.cloud.sleuth.Span[] spans = new org.springframework.cloud.sleuth.Span[len];
+ for (int i = 0; i < len; i++) {
+ spans[i] = createSleuthSpan(i * 10 + 1, i * 10 + 2, i * 10 + 3, /* exportable= */ true);
+ }
+ return spans;
+ }
+
+ private static org.springframework.cloud.sleuth.Span createSleuthSpan(
+ long tidHi, long tidLo, long sid, boolean exportable) {
+ return org.springframework.cloud.sleuth.Span.builder()
+ .name("name")
+ .traceIdHigh(tidHi)
+ .traceId(tidLo)
+ .spanId(sid)
+ .exportable(exportable)
+ .build();
+ }
+
+ private static void assertSpanEquals(
+ Span span, org.springframework.cloud.sleuth.Span sleuthSpan) {
+ assertThat(Long.parseLong(span.getContext().getTraceId().toLowerBase16().substring(0, 16), 16))
+ .isEqualTo(sleuthSpan.getTraceIdHigh());
+ assertThat(Long.parseLong(span.getContext().getTraceId().toLowerBase16().substring(16, 32), 16))
+ .isEqualTo(sleuthSpan.getTraceId());
+ assertThat(Long.parseLong(span.getContext().getSpanId().toLowerBase16(), 16))
+ .isEqualTo(sleuthSpan.getSpanId());
+ assertThat(span.getContext().getTraceOptions().isSampled())
+ .isEqualTo(sleuthSpan.isExportable());
+ }
+}
diff --git a/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanTest.java b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanTest.java
new file mode 100644
index 00000000..a4a04f97
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthSpanTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018, 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.contrib.spring.sleuth.v1x;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.springframework.cloud.sleuth.Span;
+
+/** Unit tests for {@link OpenCensusSleuthSpan}. */
+@RunWith(JUnit4.class)
+public class OpenCensusSleuthSpanTest {
+ @Test
+ public void testFromSleuthSampled() {
+ Span sleuthSpan =
+ Span.builder()
+ .name("name")
+ .traceIdHigh(12L)
+ .traceId(22L)
+ .spanId(23L)
+ .exportable(true)
+ .build();
+ assertSpanEquals(new OpenCensusSleuthSpan(sleuthSpan), sleuthSpan);
+ }
+
+ @Test
+ public void testFromSleuthNotSampled() {
+ Span sleuthSpan =
+ Span.builder()
+ .name("name")
+ .traceIdHigh(12L)
+ .traceId(22L)
+ .spanId(23L)
+ .exportable(false)
+ .build();
+ assertSpanEquals(new OpenCensusSleuthSpan(sleuthSpan), sleuthSpan);
+ }
+
+ private static final void assertSpanEquals(io.opencensus.trace.Span span, Span sleuthSpan) {
+ assertThat(span.getContext().isValid()).isTrue();
+ assertThat(Long.parseLong(span.getContext().getTraceId().toLowerBase16().substring(0, 16), 16))
+ .isEqualTo(sleuthSpan.getTraceIdHigh());
+ assertThat(Long.parseLong(span.getContext().getTraceId().toLowerBase16().substring(16, 32), 16))
+ .isEqualTo(sleuthSpan.getTraceId());
+ assertThat(Long.parseLong(span.getContext().getSpanId().toLowerBase16(), 16))
+ .isEqualTo(sleuthSpan.getSpanId());
+ assertThat(span.getContext().getTraceOptions().isSampled())
+ .isEqualTo(sleuthSpan.isExportable());
+ }
+}
diff --git a/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracerTest.java b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracerTest.java
new file mode 100644
index 00000000..924c06ee
--- /dev/null
+++ b/contrib/spring_sleuth_v1x/src/test/java/io/opencensus/contrib/spring/sleuth/v1x/OpenCensusSleuthTracerTest.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2018, 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.contrib.spring.sleuth.v1x;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Random;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.springframework.cloud.sleuth.DefaultSpanNamer;
+import org.springframework.cloud.sleuth.NoOpSpanReporter;
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.cloud.sleuth.TraceKeys;
+import org.springframework.cloud.sleuth.Tracer;
+import org.springframework.cloud.sleuth.log.NoOpSpanLogger;
+import org.springframework.cloud.sleuth.sampler.AlwaysSampler;
+
+/** Unit tests for {@link OpenCensusSleuthTracer}. */
+@RunWith(JUnit4.class)
+public class OpenCensusSleuthTracerTest {
+ private static final Tracer tracer =
+ new OpenCensusSleuthTracer(
+ new AlwaysSampler(),
+ new Random(),
+ new DefaultSpanNamer(),
+ new NoOpSpanLogger(),
+ new NoOpSpanReporter(),
+ new TraceKeys());
+
+ @After
+ @Before
+ public void verifyNotTracing() {
+ assertThat(tracer.isTracing()).isFalse();
+ }
+
+ @Test
+ public void testRootSpanAndClose() {
+ Span root = tracer.createSpan("root");
+ assertCurrentSpanIs(root);
+ assertThat(root.getSavedSpan()).isNull();
+ Span parent = tracer.close(root);
+ assertThat(parent).isNull();
+ }
+
+ @Test
+ public void testSpanStackAndClose() {
+ Span[] spans = createSpansAndAssertCurrent(3);
+ // pop the stack
+ for (int i = spans.length - 1; i >= 0; i--) {
+ assertCurrentSpanIs(spans[i]);
+ Span parent = tracer.close(spans[i]);
+ assertThat(parent).isEqualTo(spans[i].getSavedSpan());
+ }
+ }
+
+ @Test
+ public void testSpanStackAndCloseOutOfOrder() {
+ Span[] spans = createSpansAndAssertCurrent(3);
+ // try to close a non-current span
+ tracer.close(spans[spans.length - 2]);
+ assertCurrentSpanIs(spans[spans.length - 1]);
+ // pop the stack
+ for (int i = spans.length - 1; i >= 0; i--) {
+ tracer.close(spans[i]);
+ }
+ }
+
+ @Test
+ public void testDetachNull() {
+ Span parent = tracer.detach(null);
+ assertThat(parent).isNull();
+ }
+
+ @Test
+ public void testRootSpanAndDetach() {
+ Span root = tracer.createSpan("root");
+ assertCurrentSpanIs(root);
+ assertThat(root.getSavedSpan()).isNull();
+ Span parent = tracer.detach(root);
+ assertThat(parent).isNull();
+ }
+
+ @Test
+ public void testSpanStackAndDetach() {
+ Span[] spans = createSpansAndAssertCurrent(3);
+ Span parent = tracer.detach(spans[spans.length - 1]);
+ assertThat(parent).isEqualTo(spans[spans.length - 2]);
+ }
+
+ @Test
+ public void testSpanStackAndDetachOutOfOrder() {
+ Span[] spans = createSpansAndAssertCurrent(3);
+ // try to detach a non-current span
+ tracer.detach(spans[spans.length - 2]);
+ assertCurrentSpanIs(spans[spans.length - 1]);
+ Span parent = tracer.detach(spans[spans.length - 1]);
+ assertThat(parent).isEqualTo(spans[spans.length - 2]);
+ }
+
+ @Test
+ public void testContinueNull() {
+ Span span = tracer.continueSpan(null);
+ assertThat(span).isNull();
+ }
+
+ @Test
+ public void testRootSpanAndContinue() {
+ Span root = tracer.createSpan("root");
+ assertCurrentSpanIs(root);
+ tracer.detach(root);
+ Span span = tracer.continueSpan(root);
+ assertThat(span).isEqualTo(root);
+ tracer.detach(span);
+ }
+
+ @Test
+ public void testSpanStackAndContinue() {
+ Span[] spans = createSpansAndAssertCurrent(3);
+ Span original = tracer.getCurrentSpan();
+ assertThat(original).isEqualTo(spans[spans.length - 1]);
+ Span parent = tracer.detach(original);
+ assertThat(parent).isEqualTo(spans[spans.length - 2]);
+ assertThat(tracer.getCurrentSpan()).isNull();
+
+ Span continued = tracer.continueSpan(original);
+ assertCurrentSpanIs(continued);
+ assertThat(continued.getSavedSpan()).isEqualTo(parent);
+ assertThat(continued).isEqualTo(original);
+ tracer.detach(continued);
+ }
+
+ @Test
+ public void testSpanStackAndCreateAndContinue() {
+ createSpansAndAssertCurrent(3);
+ Span original = tracer.getCurrentSpan();
+ tracer.detach(original);
+ Span root = tracer.createSpan("root");
+ assertCurrentSpanIs(root);
+ Span continued = tracer.continueSpan(original);
+ assertCurrentSpanIs(continued);
+ assertThat(continued.getSavedSpan()).isEqualTo(root);
+ assertThat(continued).isEqualTo(original);
+ assertThat(continued.getSavedSpan()).isNotEqualTo(original.getSavedSpan());
+ tracer.detach(continued);
+ }
+
+ // Verifies span and associated saved span.
+ private static void assertCurrentSpanIs(Span span) {
+ assertThat(tracer.getCurrentSpan()).isEqualTo(span);
+ assertThat(tracer.getCurrentSpan().getSavedSpan()).isEqualTo(span.getSavedSpan());
+
+ assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan()).isEqualTo(span);
+ assertThat(OpenCensusSleuthSpanContextHolder.getCurrentSpan().getSavedSpan())
+ .isEqualTo(span.getSavedSpan());
+ }
+
+ private static Span[] createSpansAndAssertCurrent(int len) {
+ Span[] spans = new Span[len];
+
+ Span current = null;
+ for (int i = 0; i < len; i++) {
+ current = tracer.createSpan("span" + i, current);
+ spans[i] = current;
+ assertCurrentSpanIs(current);
+ }
+ return spans;
+ }
+}
diff --git a/contrib/zpages/README.md b/contrib/zpages/README.md
new file mode 100644
index 00000000..2a535cec
--- /dev/null
+++ b/contrib/zpages/README.md
@@ -0,0 +1,97 @@
+# OpenCensus Z-Pages
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Z-Pages for Java* is a collection of HTML pages to display stats and trace data and
+allows library configuration control.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-zpages</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-contrib-zpages:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+### Register the Z-Pages
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ ZPageHandlers.startHttpServerAndRegisterAll(8080);
+ // ... do work
+ }
+}
+```
+
+### View stats and spans on Z-Pages
+
+#### View RPC stats on /rpcz page
+
+The /rpcz page displays the canonical gRPC cumulative and interval stats broken down by RPC methods.
+Example:
+
+![rpcz-example](screenshots/rpcz-example.png)
+
+#### View measures and stats for all exported views on /statsz page
+
+The /statsz page displays measures and stats for all exported views. Views are grouped into directories
+according to their namespace. Example:
+
+![statsz-example-1](screenshots/statsz-example-1.png)
+![statsz-example-2](screenshots/statsz-example-2.png)
+
+#### View trace spans on /tracez page
+
+The /tracez page displays information about all active spans and all sampled spans based on latency
+and errors. Example:
+
+![tracez-example](screenshots/tracez-example.png)
+
+#### View and update tracing configuration on /traceconfigz page
+
+The /traceconfigz page displays information about the current active tracing configuration and
+allows users to change it. Example:
+
+![traceconfigz-example](screenshots/traceconfigz-example.png)
+
+
+### FAQ
+
+#### Why do I not see sampled spans based on latency and error codes for a given span name?
+Sampled spans based on latency and error codes are available only for registered span names.
+For more details see [SampledSpanStore][sampledspanstore-url].
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-zpages/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-zpages
+[sampledspanstore-url]: https://github.com/census-instrumentation/opencensus-java/blob/master/api/src/main/java/io/opencensus/trace/export/SampledSpanStore.java
diff --git a/contrib/zpages/build.gradle b/contrib/zpages/build.gradle
new file mode 100644
index 00000000..9648d64e
--- /dev/null
+++ b/contrib/zpages/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus Z-Pages'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.8
+ it.targetCompatibility = 1.8
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ project(':opencensus-contrib-grpc-metrics',),
+ libraries.guava
+
+ signature "org.codehaus.mojo.signature:java18:+@signature"
+}
diff --git a/contrib/zpages/screenshots/rpcz-example.png b/contrib/zpages/screenshots/rpcz-example.png
new file mode 100644
index 00000000..9d303fb1
--- /dev/null
+++ b/contrib/zpages/screenshots/rpcz-example.png
Binary files differ
diff --git a/contrib/zpages/screenshots/statsz-example-1.png b/contrib/zpages/screenshots/statsz-example-1.png
new file mode 100644
index 00000000..503a05b2
--- /dev/null
+++ b/contrib/zpages/screenshots/statsz-example-1.png
Binary files differ
diff --git a/contrib/zpages/screenshots/statsz-example-2.png b/contrib/zpages/screenshots/statsz-example-2.png
new file mode 100644
index 00000000..bb1229c8
--- /dev/null
+++ b/contrib/zpages/screenshots/statsz-example-2.png
Binary files differ
diff --git a/contrib/zpages/screenshots/traceconfigz-example.png b/contrib/zpages/screenshots/traceconfigz-example.png
new file mode 100644
index 00000000..54287683
--- /dev/null
+++ b/contrib/zpages/screenshots/traceconfigz-example.png
Binary files differ
diff --git a/contrib/zpages/screenshots/tracez-example.png b/contrib/zpages/screenshots/tracez-example.png
new file mode 100644
index 00000000..cfcf0f3c
--- /dev/null
+++ b/contrib/zpages/screenshots/tracez-example.png
Binary files differ
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/RpczZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/RpczZPageHandler.java
new file mode 100644
index 00000000..4d79fb0c
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/RpczZPageHandler.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2018, 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.contrib.zpages;
+
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_CUMULATIVE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_RESPONSE_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_STARTED_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_ERROR_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_ERROR_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_ERROR_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_FINISHED_COUNT_CUMULATIVE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_FINISHED_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_REQUEST_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_RESPONSE_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_ELAPSED_TIME_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_LATENCY_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_LATENCY_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_STARTED_COUNT_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_STARTED_COUNT_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagValue;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedMap;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** HTML page formatter for gRPC cumulative and interval stats. */
+@SuppressWarnings("deprecation")
+final class RpczZPageHandler extends ZPageHandler {
+
+ private final ViewManager viewManager;
+
+ private static final String RPCZ_URL = "/rpcz";
+ private static final String SENT = "Sent";
+ private static final String RECEIVED = "Received";
+ private static final double SECONDS_PER_MINUTE = 60.0;
+ private static final double SECONDS_PER_HOUR = 3600.0;
+ private static final double NANOS_PER_SECOND = 1e9;
+ private static final double BYTES_PER_KB = 1024;
+ private static final ImmutableList<String> RPC_STATS_TYPES =
+ ImmutableList.of(
+ "Count",
+ "Avg latency (ms)",
+ // TODO(songya): add a column for latency percentiles.
+ "Rate (rpc/s)",
+ "Input (kb/s)",
+ "Output (kb/s)",
+ "Errors");
+
+ private static final ImmutableList<View> CLIENT_RPC_CUMULATIVE_VIEWS =
+ ImmutableList.of(
+ RPC_CLIENT_ERROR_COUNT_VIEW,
+ RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW,
+ RPC_CLIENT_REQUEST_BYTES_VIEW,
+ RPC_CLIENT_RESPONSE_BYTES_VIEW,
+ RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW,
+ // The last 5 views are not used yet.
+ RPC_CLIENT_REQUEST_COUNT_VIEW,
+ RPC_CLIENT_RESPONSE_COUNT_VIEW,
+ RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_VIEW,
+ RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_VIEW,
+ RPC_CLIENT_FINISHED_COUNT_CUMULATIVE_VIEW);
+
+ private static final ImmutableList<View> SERVER_RPC_CUMULATIVE_VIEWS =
+ ImmutableList.of(
+ RPC_SERVER_ERROR_COUNT_VIEW,
+ RPC_SERVER_SERVER_LATENCY_VIEW,
+ RPC_SERVER_REQUEST_BYTES_VIEW,
+ RPC_SERVER_RESPONSE_BYTES_VIEW,
+ RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW,
+ // The last 5 views are not used yet.
+ RPC_SERVER_REQUEST_COUNT_VIEW,
+ RPC_SERVER_RESPONSE_COUNT_VIEW,
+ RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_VIEW,
+ RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_VIEW,
+ RPC_SERVER_FINISHED_COUNT_CUMULATIVE_VIEW);
+
+ // Interval views may be removed in the future.
+ private static final ImmutableList<View> CLIENT_RPC_MINUTE_VIEWS =
+ ImmutableList.of(
+ RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW,
+ RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW,
+ RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW,
+ RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW,
+ RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW,
+ // The last 5 views are not used yet.
+ RPC_CLIENT_REQUEST_COUNT_MINUTE_VIEW,
+ RPC_CLIENT_RESPONSE_COUNT_MINUTE_VIEW,
+ RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW,
+ RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW,
+ RPC_CLIENT_FINISHED_COUNT_MINUTE_VIEW);
+
+ // Interval views may be removed in the future.
+ private static final ImmutableList<View> SERVER_RPC_MINUTE_VIEWS =
+ ImmutableList.of(
+ RPC_SERVER_ERROR_COUNT_MINUTE_VIEW,
+ RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW,
+ RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW,
+ RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW,
+ RPC_SERVER_STARTED_COUNT_MINUTE_VIEW,
+ // The last 5 views are not used yet.
+ RPC_SERVER_REQUEST_COUNT_MINUTE_VIEW,
+ RPC_SERVER_RESPONSE_COUNT_MINUTE_VIEW,
+ RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_MINUTE_VIEW,
+ RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_MINUTE_VIEW,
+ RPC_SERVER_FINISHED_COUNT_MINUTE_VIEW);
+
+ // Interval views may be removed in the future.
+ private static final ImmutableList<View> CLIENT_RPC_HOUR_VIEWS =
+ ImmutableList.of(
+ RPC_CLIENT_ERROR_COUNT_HOUR_VIEW,
+ RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW,
+ RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW,
+ RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW,
+ RPC_CLIENT_STARTED_COUNT_HOUR_VIEW,
+ // The last 5 views are not used yet.
+ RPC_CLIENT_REQUEST_COUNT_HOUR_VIEW,
+ RPC_CLIENT_RESPONSE_COUNT_HOUR_VIEW,
+ RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW,
+ RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW,
+ RPC_CLIENT_FINISHED_COUNT_HOUR_VIEW);
+
+ // Interval views may be removed in the future.
+ private static final ImmutableList<View> SERVER_RPC_HOUR_VIEWS =
+ ImmutableList.of(
+ RPC_SERVER_ERROR_COUNT_HOUR_VIEW,
+ RPC_SERVER_SERVER_LATENCY_HOUR_VIEW,
+ RPC_SERVER_SERVER_ELAPSED_TIME_HOUR_VIEW,
+ RPC_SERVER_REQUEST_BYTES_HOUR_VIEW,
+ RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW,
+ RPC_SERVER_STARTED_COUNT_HOUR_VIEW,
+ // The last 5 views are not used yet.
+ RPC_SERVER_REQUEST_COUNT_HOUR_VIEW,
+ RPC_SERVER_RESPONSE_COUNT_HOUR_VIEW,
+ RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES_HOUR_VIEW,
+ RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES_HOUR_VIEW,
+ RPC_SERVER_FINISHED_COUNT_HOUR_VIEW);
+
+ @Override
+ public String getUrlPath() {
+ return RPCZ_URL;
+ }
+
+ private static void emitStyle(PrintWriter out) {
+ out.write("<style>\n");
+ out.write(Style.style);
+ out.write("</style>\n");
+ }
+
+ @Override
+ public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
+ PrintWriter out =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
+ out.write("<!DOCTYPE html>\n");
+ out.write("<html lang=\"en\"><head>\n");
+ out.write("<meta charset=\"utf-8\">\n");
+ out.write("<title>RpcZ</title>\n");
+ out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+ + "rel=\"stylesheet\">\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
+ emitStyle(out);
+ out.write("</head>\n");
+ out.write("<body>\n");
+ try {
+ emitHtmlBody(out);
+ } catch (Throwable t) {
+ out.write("Errors while generate the HTML page " + t);
+ }
+ out.write("</body>\n");
+ out.write("</html>\n");
+ out.close();
+ }
+
+ private void emitHtmlBody(PrintWriter out) {
+ Formatter formatter = new Formatter(out, Locale.US);
+ out.write(
+ "<p class=\"header\">"
+ + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
+ + "Open<span>Census</span></p>");
+ out.write("<h1>RPC Stats</h1>");
+ out.write("<p></p>");
+ emitSummaryTable(out, formatter, /* isReceived= */ false);
+ emitSummaryTable(out, formatter, /* isReceived= */ true);
+ }
+
+ private void emitSummaryTable(PrintWriter out, Formatter formatter, boolean isReceived) {
+ formatter.format(
+ "<h2><table class=\"title\"><tr align=left><td><font size=+2>"
+ + "%s</font></td></tr></table></h2>",
+ (isReceived ? RECEIVED : SENT));
+ formatter.format("<table frame=box cellspacing=0 cellpadding=2>");
+ emitSummaryTableHeader(out, formatter);
+ Map<String, StatsSnapshot> snapshots = getStatsSnapshots(isReceived);
+ for (Entry<String, StatsSnapshot> entry : snapshots.entrySet()) {
+ emitSummaryTableRows(out, formatter, entry.getValue(), entry.getKey());
+ }
+ out.write("</table>");
+ out.write("<br />");
+ }
+
+ private static void emitSummaryTableHeader(PrintWriter out, Formatter formatter) {
+ // First line.
+ formatter.format("<tr bgcolor=#A94442>");
+ out.write("<th></th><td></td>");
+ for (String rpcStatsType : RPC_STATS_TYPES) {
+ formatter.format("<th class=\"borderLB\" colspan=3>%s</th>", rpcStatsType);
+ }
+ out.write("</tr>");
+
+ // Second line.
+ formatter.format("<tr bgcolor=#A94442>");
+ out.write("<th align=left>Method</th>\n");
+ out.write("<td bgcolor=#A94442>&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ for (int i = 0; i < RPC_STATS_TYPES.size(); i++) {
+ out.write("<th class=\"borderLB\" align=center>Min.</th>\n");
+ out.write("<th class=\"borderLB\" align=center>Hr.</th>\n");
+ out.write("<th class=\"borderLB\" align=center>Tot.</th>");
+ }
+ }
+
+ private static void emitSummaryTableRows(
+ PrintWriter out, Formatter formatter, StatsSnapshot snapshot, String method) {
+ out.write("<tr>");
+ formatter.format("<td><b>%s</b></td>", method);
+ out.write("<td></td>");
+ formatter.format("<td class=\"borderLC\">%d</td>", snapshot.countLastMinute);
+ formatter.format("<td class=\"borderLC\">%d</td>", snapshot.countLastHour);
+ formatter.format("<td class=\"borderLC\">%d</td>", snapshot.countTotal);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.avgLatencyLastMinute);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.avgLatencyLastHour);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.avgLatencyTotal);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.rpcRateLastMinute);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.rpcRateLastHour);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.rpcRateTotal);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.inputRateLastMinute);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.inputRateLastHour);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.inputRateTotal);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.outputRateLastMinute);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.outputRateLastHour);
+ formatter.format("<td class=\"borderLC\">%.3f</td>", snapshot.outputRateTotal);
+ formatter.format("<td class=\"borderLC\">%d</td>", snapshot.errorsLastMinute);
+ formatter.format("<td class=\"borderLC\">%d</td>", snapshot.errorsLastHour);
+ formatter.format("<td class=\"borderLC\">%d</td>", snapshot.errorsTotal);
+ out.write("</tr>");
+ }
+
+ // Gets stats snapshot for each method.
+ private Map<String, StatsSnapshot> getStatsSnapshots(boolean isReceived) {
+ SortedMap<String, StatsSnapshot> map = Maps.newTreeMap(); // Sorted by method name.
+ if (isReceived) {
+ getStatsSnapshots(map, SERVER_RPC_CUMULATIVE_VIEWS);
+ getStatsSnapshots(map, SERVER_RPC_MINUTE_VIEWS);
+ getStatsSnapshots(map, SERVER_RPC_HOUR_VIEWS);
+ } else {
+ getStatsSnapshots(map, CLIENT_RPC_CUMULATIVE_VIEWS);
+ getStatsSnapshots(map, CLIENT_RPC_MINUTE_VIEWS);
+ getStatsSnapshots(map, CLIENT_RPC_HOUR_VIEWS);
+ }
+ return map;
+ }
+
+ private void getStatsSnapshots(Map<String, StatsSnapshot> map, List<View> views) {
+ for (View view : views) {
+ ViewData viewData = viewManager.getView(view.getName());
+ if (viewData == null) {
+ continue;
+ }
+ for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ viewData.getAggregationMap().entrySet()) {
+ TagValue tagValue;
+ List</*@Nullable*/ TagValue> tagValues = entry.getKey();
+ if (tagValues.size() == 1) {
+ tagValue = tagValues.get(0);
+ } else { // Error count views have two tag key: status and method.
+ tagValue = tagValues.get(1);
+ }
+ String method = tagValue == null ? "" : tagValue.asString();
+ StatsSnapshot snapshot = map.get(method);
+ if (snapshot == null) {
+ snapshot = new StatsSnapshot();
+ map.put(method, snapshot);
+ }
+
+ getStats(snapshot, entry.getValue(), view, viewData.getWindowData());
+ }
+ }
+ }
+
+ // Gets RPC stats by its view definition, and set it to stats snapshot.
+ private static void getStats(
+ StatsSnapshot snapshot,
+ AggregationData data,
+ View view,
+ ViewData.AggregationWindowData windowData) {
+ if (view == RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW || view == RPC_SERVER_SERVER_LATENCY_VIEW) {
+ snapshot.avgLatencyTotal = ((DistributionData) data).getMean();
+ } else if (view == RPC_CLIENT_ROUNDTRIP_LATENCY_MINUTE_VIEW
+ || view == RPC_SERVER_SERVER_LATENCY_MINUTE_VIEW) {
+ snapshot.avgLatencyLastMinute = ((AggregationData.MeanData) data).getMean();
+ } else if (view == RPC_CLIENT_ROUNDTRIP_LATENCY_HOUR_VIEW
+ || view == RPC_SERVER_SERVER_LATENCY_HOUR_VIEW) {
+ snapshot.avgLatencyLastHour = ((AggregationData.MeanData) data).getMean();
+ } else if (view == RPC_CLIENT_ERROR_COUNT_VIEW || view == RPC_SERVER_ERROR_COUNT_VIEW) {
+ snapshot.errorsTotal = ((AggregationData.MeanData) data).getCount();
+ } else if (view == RPC_CLIENT_ERROR_COUNT_MINUTE_VIEW
+ || view == RPC_SERVER_ERROR_COUNT_MINUTE_VIEW) {
+ snapshot.errorsLastMinute = ((AggregationData.MeanData) data).getCount();
+ } else if (view == RPC_CLIENT_ERROR_COUNT_HOUR_VIEW
+ || view == RPC_SERVER_ERROR_COUNT_HOUR_VIEW) {
+ snapshot.errorsLastHour = ((AggregationData.MeanData) data).getCount();
+ } else if (view == RPC_CLIENT_REQUEST_BYTES_VIEW || view == RPC_SERVER_REQUEST_BYTES_VIEW) {
+ DistributionData distributionData = (DistributionData) data;
+ snapshot.inputRateTotal =
+ distributionData.getCount()
+ * distributionData.getMean()
+ / BYTES_PER_KB
+ / getDurationInSecs((ViewData.AggregationWindowData.CumulativeData) windowData);
+ } else if (view == RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW
+ || view == RPC_SERVER_REQUEST_BYTES_MINUTE_VIEW) {
+ AggregationData.MeanData meanData = (AggregationData.MeanData) data;
+ snapshot.inputRateLastMinute =
+ meanData.getMean() * meanData.getCount() / BYTES_PER_KB / SECONDS_PER_MINUTE;
+ } else if (view == RPC_CLIENT_REQUEST_BYTES_HOUR_VIEW
+ || view == RPC_SERVER_REQUEST_BYTES_HOUR_VIEW) {
+ AggregationData.MeanData meanData = (AggregationData.MeanData) data;
+ snapshot.inputRateLastHour =
+ meanData.getMean() * meanData.getCount() / BYTES_PER_KB / SECONDS_PER_HOUR;
+ } else if (view == RPC_CLIENT_RESPONSE_BYTES_VIEW || view == RPC_SERVER_RESPONSE_BYTES_VIEW) {
+ DistributionData distributionData = (DistributionData) data;
+ snapshot.outputRateTotal =
+ distributionData.getCount()
+ * distributionData.getMean()
+ / BYTES_PER_KB
+ / getDurationInSecs((ViewData.AggregationWindowData.CumulativeData) windowData);
+ } else if (view == RPC_CLIENT_RESPONSE_BYTES_MINUTE_VIEW
+ || view == RPC_SERVER_RESPONSE_BYTES_MINUTE_VIEW) {
+ AggregationData.MeanData meanData = (AggregationData.MeanData) data;
+ snapshot.outputRateLastMinute =
+ meanData.getMean() * meanData.getCount() / BYTES_PER_KB / SECONDS_PER_MINUTE;
+ } else if (view == RPC_CLIENT_RESPONSE_BYTES_HOUR_VIEW
+ || view == RPC_SERVER_RESPONSE_BYTES_HOUR_VIEW) {
+ AggregationData.MeanData meanData = (AggregationData.MeanData) data;
+ snapshot.outputRateLastHour =
+ meanData.getMean() * meanData.getCount() / BYTES_PER_KB / SECONDS_PER_HOUR;
+ } else if (view == RPC_CLIENT_STARTED_COUNT_MINUTE_VIEW
+ || view == RPC_SERVER_STARTED_COUNT_MINUTE_VIEW) {
+ snapshot.countLastMinute = ((CountData) data).getCount();
+ snapshot.rpcRateLastMinute = snapshot.countLastMinute / SECONDS_PER_MINUTE;
+ } else if (view == RPC_CLIENT_STARTED_COUNT_HOUR_VIEW
+ || view == RPC_SERVER_STARTED_COUNT_HOUR_VIEW) {
+ snapshot.countLastHour = ((CountData) data).getCount();
+ snapshot.rpcRateLastHour = snapshot.countLastHour / SECONDS_PER_HOUR;
+ } else if (view == RPC_CLIENT_STARTED_COUNT_CUMULATIVE_VIEW
+ || view == RPC_SERVER_STARTED_COUNT_CUMULATIVE_VIEW) {
+ snapshot.countTotal = ((CountData) data).getCount();
+ snapshot.rpcRateTotal =
+ snapshot.countTotal
+ / getDurationInSecs((ViewData.AggregationWindowData.CumulativeData) windowData);
+ } // TODO(songya): compute and store latency percentiles.
+ }
+
+ // Calculates the duration of the given CumulativeData in seconds.
+ private static double getDurationInSecs(
+ ViewData.AggregationWindowData.CumulativeData cumulativeData) {
+ return toDoubleSeconds(cumulativeData.getEnd().subtractTimestamp(cumulativeData.getStart()));
+ }
+
+ // Converts a Duration to seconds. Converts the nanoseconds of the given duration to decimals of
+ // second, and adds it to the second of duration.
+ // For example, Duration.create(/* seconds */ 5, /* nanos */ 5 * 1e8) will be converted to 5.5
+ // seconds.
+ private static double toDoubleSeconds(Duration duration) {
+ return duration.getNanos() / NANOS_PER_SECOND + duration.getSeconds();
+ }
+
+ static RpczZPageHandler create(ViewManager viewManager) {
+ return new RpczZPageHandler(viewManager);
+ }
+
+ private RpczZPageHandler(ViewManager viewManager) {
+ this.viewManager = viewManager;
+ }
+
+ private static class StatsSnapshot {
+ long countLastMinute;
+ long countLastHour;
+ long countTotal;
+ double rpcRateLastMinute;
+ double rpcRateLastHour;
+ double rpcRateTotal;
+ double avgLatencyLastMinute;
+ double avgLatencyLastHour;
+ double avgLatencyTotal;
+ double inputRateLastMinute;
+ double inputRateLastHour;
+ double inputRateTotal;
+ double outputRateLastMinute;
+ double outputRateLastHour;
+ double outputRateTotal;
+ long errorsLastMinute;
+ long errorsLastHour;
+ long errorsTotal;
+ }
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/StatszZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/StatszZPageHandler.java
new file mode 100644
index 00000000..00c72d6c
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/StatszZPageHandler.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright 2018, 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.contrib.zpages;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+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.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+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.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.SortedMap;
+import javax.annotation.concurrent.GuardedBy;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** HTML page formatter for all exported {@link View}s. */
+@SuppressWarnings("deprecation")
+final class StatszZPageHandler extends ZPageHandler {
+
+ private static final Object monitor = new Object();
+
+ private final ViewManager viewManager;
+
+ // measures, cachedViews and root are created when StatszZPageHandler is initialized, and will
+ // be updated every time when there's a new View from viewManager.getAllExportedViews().
+ // viewManager.getAllExportedViews() will be called every time when the StatsZ page is
+ // re-rendered, like refreshing or navigating to other paths.
+
+ @GuardedBy("monitor")
+ private final Map<String, Measure> measures = Maps.newTreeMap();
+
+ @GuardedBy("monitor")
+ private final Set<View> cachedViews = Sets.newHashSet();
+
+ @GuardedBy("monitor")
+ private final TreeNode root = new TreeNode();
+
+ @VisibleForTesting static final String QUERY_PATH = "path";
+ private static final String STATSZ_URL = "/statsz";
+ private static final String CLASS_LARGER_TR = "directory-tr";
+ private static final String TABLE_HEADER_VIEW = "View Name";
+ private static final String TABLE_HEADER_DESCRIPTION = "Description";
+ private static final String TABLE_HEADER_MEASURE = "Measure";
+ private static final String TABLE_HEADER_AGGREGATION = "Aggregation Type";
+ private static final String TABLE_HEADER_START = "Start Time";
+ private static final String TABLE_HEADER_END = "End Time";
+ private static final String TABLE_HEADER_UNIT = "Unit";
+ private static final String TABLE_HEADER_MEASURE_TYPE = "Type";
+ private static final String TABLE_HEADER_SUM = "Sum";
+ private static final String TABLE_HEADER_COUNT = "Count";
+ private static final String TABLE_HEADER_MEAN = "Mean";
+ private static final String TABLE_HEADER_MAX = "Max";
+ private static final String TABLE_HEADER_MIN = "Min";
+ private static final String TABLE_HEADER_DEV = "Sum of Squared Deviations";
+ private static final String TABLE_HEADER_HISTOGRAM = "Histogram";
+ private static final String TABLE_HEADER_RANGE = "Range";
+ private static final String TABLE_HEADER_BUCKET_SIZE = "Bucket Size";
+ private static final String TABLE_HEADER_LAST_VALUE = "Last Value";
+ private static final long MILLIS_PER_SECOND = 1000;
+ private static final long NANOS_PER_MILLISECOND = 1000 * 1000;
+ private static final Splitter PATH_SPLITTER = Splitter.on('/');
+
+ @Override
+ public String getUrlPath() {
+ return STATSZ_URL;
+ }
+
+ @Override
+ public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
+ PrintWriter out =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
+ out.write("<!DOCTYPE html>\n");
+ out.write("<html lang=\"en\"><head>\n");
+ out.write("<meta charset=\"utf-8\">\n");
+ out.write("<title>StatsZ</title>\n");
+ out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+ + "rel=\"stylesheet\">\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
+ Formatter formatter = new Formatter(out, Locale.US);
+ emitStyles(out, formatter);
+ out.write("</head>\n");
+ out.write("<body>\n");
+ try {
+ emitHtmlBody(queryMap, out, formatter);
+ } catch (Throwable t) {
+ out.write("Errors while generate the HTML page " + t);
+ }
+ out.write("</body>\n");
+ out.write("</html>\n");
+ out.close();
+ }
+
+ private static void emitStyles(PrintWriter out, Formatter formatter) {
+ out.write("<style>");
+ out.write(Style.style);
+ formatter.format(".%s{font-size:150%%}", CLASS_LARGER_TR);
+ out.write("</style>");
+ }
+
+ private void emitHtmlBody(Map<String, String> queryMap, PrintWriter out, Formatter formatter) {
+ synchronized (monitor) {
+ groupViewsByDirectoriesAndGetMeasures(
+ viewManager.getAllExportedViews(), root, measures, cachedViews);
+ out.write(
+ "<p class=\"header\">"
+ + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
+ + "Open<span>Census</span></p>");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+ + "rel=\"stylesheet\">\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Roboto\""
+ + "rel=\"stylesheet\">\n");
+ out.write("<h1><a href='?'>StatsZ</a></h1>");
+ out.write("<p></p>");
+ String path = queryMap.get(QUERY_PATH);
+ TreeNode current = findNode(path);
+ emitDirectoryTable(current, path, out, formatter);
+ if (current != null && current.viewName != null) {
+ ViewData viewData = viewManager.getView(current.viewName);
+ emitViewData(viewData, current.viewName, out, formatter);
+ }
+ emitMeasureTable(measures, out, formatter);
+ }
+ }
+
+ // Parses view names, creates a tree that represents the directory structure and put each view
+ // under appropriate directory. Also gets measures from the given views.
+ // Directories are the namespaces in view name, separated by '/'.
+ private static void groupViewsByDirectoriesAndGetMeasures(
+ Set<View> views, TreeNode root, Map<String, Measure> measures, Set<View> cachedViews) {
+ for (View view : views) {
+ if (cachedViews.contains(view)) {
+ continue;
+ }
+ cachedViews.add(view);
+
+ List<String> dirs = PATH_SPLITTER.splitToList(view.getName().asString());
+ TreeNode node = root;
+ for (int i = 0; i < dirs.size(); i++) {
+ if (node == null) {
+ break; // Should never happen. Work around the nullness checker.
+ }
+ String dir = dirs.get(i);
+ if ("".equals(dir) && i == 0) {
+ continue; // In case view name starts with a '/'.
+ }
+ node.views++;
+ if (i != dirs.size() - 1) { // Non-leaf node (directory node)
+ node.children.putIfAbsent(dir, new TreeNode());
+ node = node.children.get(dir);
+ } else { // Leaf node (view node)
+ node.children.putIfAbsent(dir, new TreeNode(view.getName()));
+ }
+ }
+
+ Measure measure = view.getMeasure();
+ measures.putIfAbsent(measure.getName(), measure);
+ }
+ }
+
+ @GuardedBy("monitor")
+ private void emitDirectoryTable(
+ /*@Nullable*/ TreeNode currentNode,
+ /*@Nullable*/ String path,
+ PrintWriter out,
+ Formatter formatter) {
+ out.write("<h2 style=\"margin-bottom:0;\">Views</h2>");
+ if (currentNode == null) {
+ formatter.format(
+ "<p><font size=+2>Directory not found: %s. Return to root.</font></p>", path);
+ currentNode = root;
+ }
+ if (currentNode == root || path == null) {
+ path = "";
+ }
+ emitDirectoryHeader(path, out, formatter);
+ out.write("<table class=\"title\" cellspacing=0 cellpadding=0>");
+ for (Entry<String, TreeNode> entry : currentNode.children.entrySet()) {
+ TreeNode child = entry.getValue();
+ String relativePath = entry.getKey();
+ if (child.viewName == null) { // Directory node, emit a row for directory.
+ formatter.format(
+ "<tr class=\"direct\"><td>Directory: <a href='?%s=%s'>%s</a> (%d %s)</td></tr>",
+ QUERY_PATH,
+ path + '/' + relativePath,
+ relativePath,
+ child.views,
+ child.views > 1 ? "views" : "view");
+ } else { // View node, emit a row for view.
+ String viewName = child.viewName.asString();
+ formatter.format(
+ "<tr class=\"direct\"><td>View: <a href='?%s=%s'>%s</a></td></tr>",
+ QUERY_PATH, path + '/' + relativePath, viewName);
+ }
+ }
+ out.write("</table>");
+ out.write("<p></p>");
+ }
+
+ // Searches the TreeNode whose absolute path matches the given path, started from root.
+ // Returns null if such a TreeNode doesn't exist.
+ @GuardedBy("monitor")
+ private /*@Nullable*/ TreeNode findNode(/*@Nullable*/ String path) {
+ if (Strings.isNullOrEmpty(path) || "/".equals(path)) { // Go back to the root directory.
+ return root;
+ } else {
+ List<String> dirs = PATH_SPLITTER.splitToList(path);
+ TreeNode node = root;
+ for (int i = 0; i < dirs.size(); i++) {
+ String dir = dirs.get(i);
+ if ("".equals(dir) && i == 0) {
+ continue; // Skip the first "", the path of root node.
+ }
+ if (!node.children.containsKey(dir)) {
+ return null;
+ } else {
+ node = node.children.get(dir);
+ }
+ }
+ return node;
+ }
+ }
+
+ private static void emitDirectoryHeader(String path, PrintWriter out, Formatter formatter) {
+ List<String> dirs = PATH_SPLITTER.splitToList(path);
+ StringBuilder currentPath = new StringBuilder("");
+ out.write("<h3>Current Path: ");
+ for (int i = 0; i < dirs.size(); i++) {
+ String dir = dirs.get(i);
+ currentPath.append(dir);
+ // create links to navigate back to parent directories.
+ formatter.format("<a href='?%s=%s'>%s</a>", QUERY_PATH, currentPath.toString(), dir + '/');
+ currentPath.append('/');
+ }
+ out.write("</h3>");
+ }
+
+ private static void emitViewData(
+ /*@Nullable*/ ViewData viewData, View.Name viewName, PrintWriter out, Formatter formatter) {
+ if (viewData == null) {
+ formatter.format("<p class=\"view\">No Stats found for View %s.</p>", viewName.asString());
+ return;
+ }
+ View view = viewData.getView();
+ emitViewInfo(view, viewData.getWindowData(), out, formatter);
+ formatter.format("<p class=\"view\">Stats for View: %s</p>", view.getName().asString());
+
+ formatter.format("<table cellspacing=0 cellpadding=0>");
+ emitViewDataTableHeader(view, out, formatter);
+ for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ viewData.getAggregationMap().entrySet()) {
+ emitViewDataRow(view, entry, out, formatter);
+ }
+ out.write("</table>");
+ out.write("<p></p>");
+ }
+
+ private static void emitViewInfo(
+ View view, ViewData.AggregationWindowData windowData, PrintWriter out, Formatter formatter) {
+ formatter.format("<table width=100%% cellspacing=0 cellpadding=0>");
+ emitViewInfoHeader(out, formatter);
+
+ out.write("<tbody>");
+ out.write("<tr>"); // One row that represents the selected view.
+ formatter.format("<td>%s</td>", view.getName().asString());
+ formatter.format("<td class=\"borderLL\">%s</td>", view.getDescription());
+ formatter.format("<td class=\"borderLL\">%s</td>", view.getMeasure().getName());
+ String aggregationType =
+ view.getAggregation()
+ .match(
+ Functions.returnConstant("Sum"),
+ Functions.returnConstant("Count"),
+ Functions.returnConstant("Distribution"),
+ Functions.returnConstant("Last Value"),
+ new Function<Aggregation, String>() {
+ @Override
+ public String apply(Aggregation arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before
+ // that
+ // we need to continue supporting Mean, since it could still be used by users
+ // and some
+ // deprecated RPC views.
+ if (arg instanceof Aggregation.Mean) {
+ return "Mean";
+ }
+ throw new AssertionError();
+ }
+ });
+ formatter.format("<td class=\"borderLL\">%s</td>", aggregationType);
+ windowData.match(
+ new Function<ViewData.AggregationWindowData.CumulativeData, Void>() {
+ @Override
+ public Void apply(ViewData.AggregationWindowData.CumulativeData arg) {
+ formatter.format("<td class=\"borderLL\">%s</td>", toDate(arg.getStart()));
+ formatter.format("<td class=\"borderLL\">%s</td>", toDate(arg.getEnd()));
+ return null;
+ }
+ },
+ Functions.</*@Nullable*/ Void>throwAssertionError(), // No interval views will be displayed.
+ Functions.</*@Nullable*/ Void>throwAssertionError());
+ out.write("</tr>");
+ out.write("</tbody>");
+ out.write("</table>");
+ out.write("<p></p>");
+ }
+
+ private static Date toDate(Timestamp timestamp) {
+ return Date.from(
+ Instant.ofEpochMilli(
+ timestamp.getSeconds() * MILLIS_PER_SECOND
+ + timestamp.getNanos() / NANOS_PER_MILLISECOND));
+ }
+
+ private static void emitViewInfoHeader(PrintWriter out, Formatter formatter) {
+ out.write("<thead>");
+ out.write("<tr>");
+ formatter.format("<th colspan=1 align=left>%s</th>", TABLE_HEADER_VIEW);
+ formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_DESCRIPTION);
+ formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_MEASURE);
+ formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_AGGREGATION);
+ formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_START);
+ formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_END);
+ out.write("</tr>");
+ out.write("</thead>");
+ }
+
+ private static void emitViewDataTableHeader(View view, PrintWriter out, Formatter formatter) {
+ out.write("<thead>");
+ out.write("<tr>");
+ for (TagKey tagKey : view.getColumns()) {
+ formatter.format("<th class=\"borderRL\">TagKey: %s (string)</th>", tagKey.getName());
+ }
+ String unit = view.getMeasure().getUnit();
+ view.getAggregation()
+ .match(
+ new Function<Sum, Void>() {
+ @Override
+ public Void apply(Sum arg) {
+ formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_SUM, unit);
+ return null;
+ }
+ },
+ new Function<Count, Void>() {
+ @Override
+ public Void apply(Count arg) {
+ formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
+ return null;
+ }
+ },
+ new Function<Distribution, Void>() {
+ @Override
+ public Void apply(Distribution arg) {
+ formatter.format("<th>%s, %s</th>", TABLE_HEADER_MEAN, unit);
+ formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
+ formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_MAX, unit);
+ formatter.format("<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_MIN, unit);
+ formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_DEV);
+ formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_HISTOGRAM);
+ return null;
+ }
+ },
+ new Function<LastValue, Void>() {
+ @Override
+ public Void apply(LastValue arg) {
+ formatter.format(
+ "<th class=\"borderL\">%s, %s</th>", TABLE_HEADER_LAST_VALUE, unit);
+ return null;
+ }
+ },
+ new Function<Aggregation, Void>() {
+ @Override
+ public Void apply(Aggregation arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before
+ // that
+ // we need to continue supporting Mean, since it could still be used by users and
+ // some
+ // deprecated RPC views.
+ if (arg instanceof Aggregation.Mean) {
+ formatter.format("<th>%s, %s</th>", TABLE_HEADER_MEAN, unit);
+ formatter.format("<th class=\"borderL\">%s</th>", TABLE_HEADER_COUNT);
+ return null;
+ }
+ throw new IllegalArgumentException("Unknown Aggregation.");
+ }
+ });
+ out.write("</tr>");
+ out.write("</thead>");
+ }
+
+ private static void emitViewDataRow(
+ View view,
+ Entry<List</*@Nullable*/ TagValue>, AggregationData> entry,
+ PrintWriter out,
+ Formatter formatter) {
+ out.write("<tr>");
+ for (/*@Nullable*/ TagValue tagValue : entry.getKey()) {
+ String tagValueStr = tagValue == null ? "" : tagValue.asString();
+ formatter.format("<td class=\"borderRL\">%s</td>", tagValueStr);
+ }
+ entry
+ .getValue()
+ .match(
+ new Function<SumDataDouble, Void>() {
+ @Override
+ public Void apply(SumDataDouble arg) {
+ formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getSum());
+ return null;
+ }
+ },
+ new Function<SumDataLong, Void>() {
+ @Override
+ public Void apply(SumDataLong arg) {
+ formatter.format("<td class=\"borderLL\">%d</td>", arg.getSum());
+ return null;
+ }
+ },
+ new Function<CountData, Void>() {
+ @Override
+ public Void apply(CountData arg) {
+ formatter.format("<td class=\"borderLL\">%d</td>", arg.getCount());
+ return null;
+ }
+ },
+ new Function<DistributionData, Void>() {
+ @Override
+ public Void apply(DistributionData arg) {
+ checkArgument(
+ view.getAggregation() instanceof Distribution, "Distribution expected.");
+ formatter.format("<td>%.3f</td>", arg.getMean());
+ formatter.format("<td class=\"borderLL\">%d</td>", arg.getCount());
+ formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getMax());
+ formatter.format("<td class=\"borderLL\">%.3f</td>", arg.getMin());
+ formatter.format(
+ "<td class=\"borderLL\">%.3f</td>", arg.getSumOfSquaredDeviations());
+ emitHistogramBuckets(
+ ((Distribution) view.getAggregation()).getBucketBoundaries().getBoundaries(),
+ arg.getBucketCounts(),
+ out,
+ formatter);
+ return null;
+ }
+ },
+ new Function<LastValueDataDouble, Void>() {
+ @Override
+ public Void apply(LastValueDataDouble arg) {
+ formatter.format("<td>%.3f</td>", arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<LastValueDataLong, Void>() {
+ @Override
+ public Void apply(LastValueDataLong arg) {
+ formatter.format("<td>%d</td>", arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<AggregationData, Void>() {
+ @Override
+ public Void apply(AggregationData arg) {
+ if (arg instanceof AggregationData.MeanData) {
+ AggregationData.MeanData meanData = (AggregationData.MeanData) arg;
+ formatter.format("<td>%.3f</td>", meanData.getMean());
+ formatter.format("<td class=\"borderLL\">%d</td>", meanData.getCount());
+ return null;
+ }
+ throw new IllegalArgumentException("Unknown Aggregation.");
+ }
+ });
+ out.write("</tr>");
+ }
+
+ private static void emitHistogramBuckets(
+ List<Double> bucketBoundaries,
+ List<Long> bucketCounts,
+ PrintWriter out,
+ Formatter formatter) {
+ checkArgument(
+ bucketBoundaries.size() == bucketCounts.size() - 1,
+ "Bucket boundaries and counts don't match");
+ out.write("<td class=\"borderLL\">");
+ out.write("<table>");
+ formatter.format(
+ "<thead><tr><th>%s</th><th>%s</th></tr></thead>",
+ TABLE_HEADER_RANGE, TABLE_HEADER_BUCKET_SIZE);
+ out.write("<tbody>");
+ for (int i = 0; i < bucketCounts.size(); i++) {
+ double low = i == 0 ? Double.NEGATIVE_INFINITY : bucketBoundaries.get(i - 1);
+ double high =
+ i == bucketCounts.size() - 1 ? Double.POSITIVE_INFINITY : bucketBoundaries.get(i);
+ out.write("<tr>");
+ formatter.format("<td>[%.3f...%.3f)</td>", low, high);
+ formatter.format("<td>%d</td>", bucketCounts.get(i));
+ out.write("</tr>");
+ }
+ out.write("</tbody>");
+ out.write("</table>");
+ out.write("</td>");
+ }
+
+ private static void emitMeasureTable(
+ Map<String, Measure> measures, PrintWriter out, Formatter formatter) {
+ out.write("<h2>Measures with Views</h2>");
+ out.write("<p>Below are the measures used in registered views.</p>");
+ out.write("<p></p>");
+ formatter.format("<table cellspacing=0 cellpadding=0>");
+ emitMeasureTableHeader(out, formatter);
+ out.write("<tbody>");
+ for (Entry<String, Measure> entry : measures.entrySet()) {
+ emitMeasureTableRow(entry.getValue(), out, formatter);
+ }
+ out.write("</tbody>");
+ out.write("</table>");
+ out.write("<p></p>");
+ }
+
+ private static void emitMeasureTableHeader(PrintWriter out, Formatter formatter) {
+ out.write("<thead>");
+ out.write("<tr>");
+ formatter.format("<th colspan=1>%s</th>", TABLE_HEADER_MEASURE);
+ formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_DESCRIPTION);
+ formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_UNIT);
+ formatter.format("<th colspan=1 class=\"borderL\">%s</th>", TABLE_HEADER_MEASURE_TYPE);
+ out.write("</tr>");
+ out.write("</thead>");
+ }
+
+ private static void emitMeasureTableRow(Measure measure, PrintWriter out, Formatter formatter) {
+ out.write("<tr>");
+ formatter.format("<td><b>%s</b></td>", measure.getName());
+ formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measure.getDescription());
+ formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measure.getUnit());
+ String measureType =
+ measure.match(
+ Functions.returnConstant("Double"),
+ Functions.returnConstant("Long"),
+ Functions.throwAssertionError());
+ formatter.format("<td class=\"borderLL\">%s&nbsp;</td>", measureType);
+ out.write("</tr>");
+ }
+
+ static StatszZPageHandler create(ViewManager viewManager) {
+ return new StatszZPageHandler(viewManager);
+ }
+
+ private StatszZPageHandler(ViewManager viewManager) {
+ this.viewManager = viewManager;
+ }
+
+ /*
+ * TreeNode for storing the structure of views and directories that they're in. Think of this as
+ * file descriptors for view: non-leaf nodes are directories which may contain views or other
+ * directories, and leaf nodes are the ones with actual information on views. Each non-leaf node
+ * also has the number of views under its directory.
+ */
+ private static class TreeNode {
+ // Only leaf nodes have views.
+ @javax.annotation.Nullable final View.Name viewName;
+
+ // A mapping from relative path to children TreeNodes. Sorted by the relative path.
+ SortedMap<String, TreeNode> children = Maps.newTreeMap();
+
+ // The number of views that a directory contains. 0 for leaf node.
+ int views = 0;
+
+ TreeNode() {
+ this.viewName = null;
+ }
+
+ TreeNode(View.Name viewName) {
+ this.viewName = checkNotNull(viewName, "view name");
+ }
+ }
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/Style.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/Style.java
new file mode 100644
index 00000000..015b83df
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/Style.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018, 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.contrib.zpages;
+
+final class Style {
+ private Style() {}
+
+ static String style =
+ "body{font-family: 'Roboto',sans-serif;"
+ + "font-size: 14px;background-color: #F2F4EC;}"
+ + "h1{color: #3D3D3D;text-align: center;margin-bottom: 20px;}"
+ + "p{padding: 0 0.5em;color: #3D3D3D;}"
+ + "h2{color: #3D3D3D;font-size: 1.5em;background-color: #FFF;"
+ + "line-height: 2.0;margin-bottom: 0;padding: 0 0.5em;}"
+ + "h3{font-size:16px;padding:0 0.5em;margin-top:6px;margin-bottom:25px;}"
+ + "a{color:#A94442;}"
+ + "p.header{font-family: 'Open Sans', sans-serif;top: 0;left: 0;width: 100%;"
+ + "height: 60px;vertical-align: middle;color: #C1272D;font-size: 22pt;}"
+ + "p.view{font-size: 20px;margin-bottom: 0;}"
+ + ".header span{color: #3D3D3D;}"
+ + "img.oc{vertical-align: middle;}"
+ + "table{width: 100%;color: #FFF;background-color: #FFF;overflow: hidden;"
+ + "margin-bottom: 30px;margin-top: 0;border-bottom: 1px solid #3D3D3D;"
+ + "border-left: 1px solid #3D3D3D;border-right: 1px solid #3D3D3D;}"
+ + "table.title{width:100%;color:#3D3D3D;background-color:#FFF;"
+ + "border:none;line-height:2.0;margin-bottom:0;}"
+ + "thead{color: #FFF;background-color: #A94442;"
+ + "line-height:3.0;padding:0 0.5em;}"
+ + "th{color: #FFF;background-color: #A94442;"
+ + "line-height:3.0;padding:0 0.5em;}"
+ + "th.borderL{border-left:1px solid #FFF; text-align:left;}"
+ + "th.borderRL{border-right:1px solid #FFF; text-align:left;}"
+ + "th.borderLB{border-left:1px solid #FFF;"
+ + "border-bottom:1px solid #FFF;margin:0 10px;}"
+ + "tr.direct{font-size:16px;padding:0 0.5em;background-color:#F2F4EC;}"
+ + "tr:nth-child(even){background-color: #F2F2F2;}"
+ + "td{color: #3D3D3D;line-height: 2.0;text-align: left;padding: 0 0.5em;}"
+ + "td.borderLC{border-left:1px solid #3D3D3D;text-align:center;}"
+ + "td.borderLL{border-left:1px solid #3D3D3D;text-align:left;}"
+ + "td.borderRL{border-right:1px solid #3D3D3D;text-align:left;}"
+ + "td.borderRW{border-right:1px solid #FFF}"
+ + "td.borderLW{border-left:1px solid #FFF;}"
+ + "td.centerW{text-align:center;color:#FFF;}"
+ + "td.center{text-align:center;color:#3D3D3D;}"
+ + "tr.bgcolor{background-color:#A94442;}"
+ + "h1.left{text-align:left;margin-left:20px;}"
+ + "table.small{width:40%;background-color:#FFF;"
+ + "margin-left:20px;margin-bottom:30px;}"
+ + "table.small{width:40%;background-color:#FFF;"
+ + "margin-left:20px;margin-bottom:30px;}"
+ + "td.col_headR{background-color:#A94442;"
+ + "line-height:3.0;color:#FFF;border-right:1px solid #FFF;}"
+ + "td.col_head{background-color:#A94442;"
+ + "line-height:3.0;color:#FFF;}"
+ + "b.title{margin-left:20px;font-weight:bold;line-height:2.0;}"
+ + "input.button{margin-left:20px;margin-top:4px;"
+ + "font-size:20px;width:80px;height:60px;}"
+ + "td.head{text-align:center;color:#FFF;line-height:3.0;}";
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TraceConfigzZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TraceConfigzZPageHandler.java
new file mode 100644
index 00000000..2a02cca6
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TraceConfigzZPageHandler.java
@@ -0,0 +1,223 @@
+/*
+ * 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.contrib.zpages;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.common.base.Charsets;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Map;
+
+// TODO(bdrutu): Add tests.
+// TODO(hailongwen): Remove the usage of `NetworkEvent` in the future.
+/**
+ * HTML page formatter for tracing config. The page displays information about the current active
+ * tracing configuration and allows users to change it.
+ */
+final class TraceConfigzZPageHandler extends ZPageHandler {
+ private static final String TRACE_CONFIGZ_URL = "/traceconfigz";
+ private final TraceConfig traceConfig;
+
+ private static final String CHANGE = "change";
+ private static final String PERMANENT_CHANGE = "permanently";
+ private static final String RESTORE_DEFAULT_CHANGE = "restore_default";
+ private static final String QUERY_COMPONENT_SAMPLING_PROBABILITY = "samplingprobability";
+ private static final String QUERY_COMPONENT_MAX_NUMBER_OF_ATTRIBUTES = "maxnumberofattributes";
+ private static final String QUERY_COMPONENT_MAX_NUMBER_OF_ANNOTATIONS = "maxnumberofannotations";
+ private static final String QUERY_COMPONENT_MAX_NUMBER_OF_NETWORK_EVENTS =
+ "maxnumberofnetworkevents";
+ private static final String QUERY_COMPONENT_MAX_NUMBER_OF_LINKS = "maxnumberoflinks";
+
+ // TODO(bdrutu): Use post.
+ // TODO(bdrutu): Refactor this to not use a big "printf".
+ private static final String TRACECONFIGZ_FORM_BODY =
+ "<form action=/traceconfigz method=get>%n"
+ // Permanently changes table.
+ + "<table class=\"small\" rules=\"all\">%n"
+ + "<td colspan=\"3\" class=\"col_head\">Permanently change "
+ + "<input type=\"hidden\" name=\"%s\" value=\"%s\"></td>%n"
+ + "<tr><td>SamplingProbability to</td> "
+ + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%s)</td>%n"
+ + "<tr><td>MaxNumberOfAttributes to</td> "
+ + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%d)</td>%n"
+ + "<tr><td>MaxNumberOfAnnotations to</td>"
+ + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%d)</td>%n"
+ + "<tr><td>MaxNumberOfNetworkEvents to</td> "
+ + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%d)</td>%n"
+ + "<tr><td>MaxNumberOfLinks to</td>"
+ + "<td><input type=text size=15 name=%s value=\"\"></td> <td>(%d)</td>%n"
+ + "</table>%n"
+ // Submit button.
+ + "<input class=\"button\" type=submit value=Submit>%n"
+ + "</form>";
+
+ private static final String RESTORE_DEFAULT_FORM_BODY =
+ "<form action=/traceconfigz method=get>%n"
+ // Restore to default.
+ + "<b class=\"title\">Restore default</b> %n"
+ + "<input type=\"hidden\" name=\"%s\" value=\"%s\"></td>%n"
+ + "</br>%n"
+ // Reset button.
+ + "<input class=\"button\" type=submit value=Reset>%n"
+ + "</form>";
+
+ static TraceConfigzZPageHandler create(TraceConfig traceConfig) {
+ return new TraceConfigzZPageHandler(traceConfig);
+ }
+
+ @Override
+ public String getUrlPath() {
+ return TRACE_CONFIGZ_URL;
+ }
+
+ private static void emitStyle(PrintWriter out) {
+ out.write("<style>\n");
+ out.write(Style.style);
+ out.write("</style>\n");
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
+ PrintWriter out =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
+ out.write("<!DOCTYPE html>\n");
+ out.write("<html lang=\"en\"><head>\n");
+ out.write("<meta charset=\"utf-8\">\n");
+ out.write("<title>TraceConfigZ</title>\n");
+ out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+ + "rel=\"stylesheet\">\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
+ emitStyle(out);
+ out.write("</head>\n");
+ out.write("<body>\n");
+ out.write(
+ "<p class=\"header\">"
+ + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
+ + "Open<span>Census</span></p>");
+ out.write("<h1 class=\"left\">Trace Configuration</h1>");
+ out.write("<p></p>");
+ try {
+ // Work that can throw exceptions.
+ maybeApplyChanges(queryMap);
+ } finally {
+ // TODO(bdrutu): Maybe display to the page if an exception happened.
+ // Display the page in any case.
+ out.printf(
+ TRACECONFIGZ_FORM_BODY,
+ CHANGE,
+ PERMANENT_CHANGE,
+ QUERY_COMPONENT_SAMPLING_PROBABILITY,
+ "0.0001", // TODO(bdrutu): Get this from the default sampler (if possible).
+ QUERY_COMPONENT_MAX_NUMBER_OF_ATTRIBUTES,
+ TraceParams.DEFAULT.getMaxNumberOfAttributes(),
+ QUERY_COMPONENT_MAX_NUMBER_OF_ANNOTATIONS,
+ TraceParams.DEFAULT.getMaxNumberOfAnnotations(),
+ QUERY_COMPONENT_MAX_NUMBER_OF_NETWORK_EVENTS,
+ TraceParams.DEFAULT.getMaxNumberOfNetworkEvents(),
+ QUERY_COMPONENT_MAX_NUMBER_OF_LINKS,
+ TraceParams.DEFAULT.getMaxNumberOfLinks());
+ out.write("<br>\n");
+ out.printf(RESTORE_DEFAULT_FORM_BODY, CHANGE, RESTORE_DEFAULT_CHANGE);
+ out.write("<br>\n");
+ emitTraceParamsTable(traceConfig.getActiveTraceParams(), out);
+ out.write("</body>\n");
+ out.write("</html>\n");
+ out.close();
+ }
+ }
+
+ // If this is a supported change (currently only permanent changes are supported) apply it.
+ @SuppressWarnings("deprecation")
+ private void maybeApplyChanges(Map<String, String> queryMap) {
+ String changeStr = queryMap.get(CHANGE);
+ if (PERMANENT_CHANGE.equals(changeStr)) {
+ TraceParams.Builder traceParamsBuilder = traceConfig.getActiveTraceParams().toBuilder();
+ String samplingProbabilityStr = queryMap.get(QUERY_COMPONENT_SAMPLING_PROBABILITY);
+ if (!isNullOrEmpty(samplingProbabilityStr)) {
+ double samplingProbability = Double.parseDouble(samplingProbabilityStr);
+ traceParamsBuilder.setSampler(Samplers.probabilitySampler(samplingProbability));
+ }
+ String maxNumberOfAttributesStr = queryMap.get(QUERY_COMPONENT_MAX_NUMBER_OF_ATTRIBUTES);
+ if (!isNullOrEmpty(maxNumberOfAttributesStr)) {
+ int maxNumberOfAttributes = Integer.parseInt(maxNumberOfAttributesStr);
+ traceParamsBuilder.setMaxNumberOfAttributes(maxNumberOfAttributes);
+ }
+ String maxNumberOfAnnotationsStr = queryMap.get(QUERY_COMPONENT_MAX_NUMBER_OF_ANNOTATIONS);
+ if (!isNullOrEmpty(maxNumberOfAnnotationsStr)) {
+ int maxNumberOfAnnotations = Integer.parseInt(maxNumberOfAnnotationsStr);
+ traceParamsBuilder.setMaxNumberOfAnnotations(maxNumberOfAnnotations);
+ }
+ String maxNumberOfNetworkEventsStr =
+ queryMap.get(QUERY_COMPONENT_MAX_NUMBER_OF_NETWORK_EVENTS);
+ if (!isNullOrEmpty(maxNumberOfNetworkEventsStr)) {
+ int maxNumberOfNetworkEvents = Integer.parseInt(maxNumberOfNetworkEventsStr);
+ traceParamsBuilder.setMaxNumberOfNetworkEvents(maxNumberOfNetworkEvents);
+ }
+ String maxNumverOfLinksStr = queryMap.get(QUERY_COMPONENT_MAX_NUMBER_OF_LINKS);
+ if (!isNullOrEmpty(maxNumverOfLinksStr)) {
+ int maxNumberOfLinks = Integer.parseInt(maxNumverOfLinksStr);
+ traceParamsBuilder.setMaxNumberOfLinks(maxNumberOfLinks);
+ }
+ traceConfig.updateActiveTraceParams(traceParamsBuilder.build());
+ } else if (RESTORE_DEFAULT_CHANGE.equals(changeStr)) {
+ traceConfig.updateActiveTraceParams(TraceParams.DEFAULT);
+ }
+ }
+
+ // Prints a table to a PrintWriter that shows existing trace parameters.
+ @SuppressWarnings("deprecation")
+ private static void emitTraceParamsTable(TraceParams params, PrintWriter out) {
+ out.write(
+ "<b class=\"title\">Active tracing parameters:</b><br>\n"
+ + "<table class=\"small\" rules=\"all\">\n"
+ + " <tr>\n"
+ + " <td class=\"col_headR\">Name</td>\n"
+ + " <td class=\"col_head\">Value</td>\n"
+ + " </tr>\n");
+ out.printf(
+ " <tr>%n <td>Sampler</td>%n <td>%s</td>%n </tr>%n",
+ params.getSampler().getDescription());
+ out.printf(
+ " <tr>%n <td>MaxNumberOfAttributes</td>%n <td>%d</td>%n </tr>%n",
+ params.getMaxNumberOfAttributes());
+ out.printf(
+ " <tr>%n <td>MaxNumberOfAnnotations</td>%n <td>%d</td>%n </tr>%n",
+ params.getMaxNumberOfAnnotations());
+ out.printf(
+ " <tr>%n <td>MaxNumberOfNetworkEvents</td>%n <td>%d</td>%n </tr>%n",
+ params.getMaxNumberOfNetworkEvents());
+ out.printf(
+ " <tr>%n <td>MaxNumberOfLinks</td>%n <td>%d</td>%n </tr>%n",
+ params.getMaxNumberOfLinks());
+
+ out.write("</table>\n");
+ }
+
+ private TraceConfigzZPageHandler(TraceConfig traceConfig) {
+ this.traceConfig = traceConfig;
+ }
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TracezZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TracezZPageHandler.java
new file mode 100644
index 00000000..f6a36996
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/TracezZPageHandler.java
@@ -0,0 +1,699 @@
+/*
+ * 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.contrib.zpages;
+
+import static com.google.common.html.HtmlEscapers.htmlEscaper;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SampledSpanStore;
+import io.opencensus.trace.export.SampledSpanStore.ErrorFilter;
+import io.opencensus.trace.export.SampledSpanStore.LatencyBucketBoundaries;
+import io.opencensus.trace.export.SampledSpanStore.LatencyFilter;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+// TODO(hailongwen): remove the usage of `NetworkEvent` in the future.
+/**
+ * HTML page formatter for tracing debug. The page displays information about all active spans and
+ * all sampled spans based on latency and errors.
+ *
+ * <p>It prints a summary table which contains one row for each span name and data about number of
+ * active and sampled spans.
+ */
+final class TracezZPageHandler extends ZPageHandler {
+ private enum RequestType {
+ RUNNING(0),
+ FINISHED(1),
+ FAILED(2),
+ UNKNOWN(-1);
+
+ private final int value;
+
+ RequestType(int value) {
+ this.value = value;
+ }
+
+ static RequestType fromString(String str) {
+ int value = Integer.parseInt(str);
+ switch (value) {
+ case 0:
+ return RUNNING;
+ case 1:
+ return FINISHED;
+ case 2:
+ return FAILED;
+ default:
+ return UNKNOWN;
+ }
+ }
+
+ int getValue() {
+ return value;
+ }
+ }
+
+ private static final String TRACEZ_URL = "/tracez";
+ private static final Tracer tracer = Tracing.getTracer();
+ // Color to use for zebra-striping.
+ private static final String ZEBRA_STRIPE_COLOR = "#FFF";
+ // Color for sampled traceIds.
+ private static final String SAMPLED_TRACE_ID_COLOR = "#C1272D";
+ // Color for not sampled traceIds
+ private static final String NOT_SAMPLED_TRACE_ID_COLOR = "black";
+ // The header for span name.
+ private static final String HEADER_SPAN_NAME = "zspanname";
+ // The header for type (running = 0, latency = 1, error = 2) to display.
+ private static final String HEADER_SAMPLES_TYPE = "ztype";
+ // The header for sub-type:
+ // * for latency based samples [0, 8] representing the latency buckets, where 0 is the first one;
+ // * for error based samples [0, 15], 0 - means all, otherwise the error code;
+ private static final String HEADER_SAMPLES_SUB_TYPE = "zsubtype";
+ // Map from LatencyBucketBoundaries to the human string displayed on the UI for each bucket.
+ private static final Map<LatencyBucketBoundaries, String> LATENCY_BUCKET_BOUNDARIES_STRING_MAP =
+ buildLatencyBucketBoundariesStringMap();
+ @javax.annotation.Nullable private final RunningSpanStore runningSpanStore;
+ @javax.annotation.Nullable private final SampledSpanStore sampledSpanStore;
+
+ private TracezZPageHandler(
+ @javax.annotation.Nullable RunningSpanStore runningSpanStore,
+ @javax.annotation.Nullable SampledSpanStore sampledSpanStore) {
+ this.runningSpanStore = runningSpanStore;
+ this.sampledSpanStore = sampledSpanStore;
+ }
+
+ /**
+ * Constructs a new {@code TracezZPageHandler}.
+ *
+ * @param runningSpanStore the instance of the {@code RunningSpanStore} to be used.
+ * @param sampledSpanStore the instance of the {@code SampledSpanStore} to be used.
+ * @return a new {@code TracezZPageHandler}.
+ */
+ static TracezZPageHandler create(
+ @javax.annotation.Nullable RunningSpanStore runningSpanStore,
+ @javax.annotation.Nullable SampledSpanStore sampledSpanStore) {
+ return new TracezZPageHandler(runningSpanStore, sampledSpanStore);
+ }
+
+ @Override
+ public String getUrlPath() {
+ return TRACEZ_URL;
+ }
+
+ private static void emitStyle(PrintWriter out) {
+ out.write("<style>\n");
+ out.write(Style.style);
+ out.write("</style>\n");
+ }
+
+ @Override
+ public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
+ PrintWriter out =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
+ out.write("<!DOCTYPE html>\n");
+ out.write("<html lang=\"en\"><head>\n");
+ out.write("<meta charset=\"utf-8\">\n");
+ out.write("<title>TraceZ</title>\n");
+ out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+ + "rel=\"stylesheet\">\n");
+ out.write(
+ "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
+ emitStyle(out);
+ out.write("</head>\n");
+ out.write("<body>\n");
+ out.write(
+ "<p class=\"header\">"
+ + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
+ + "Open<span>Census</span></p>");
+ out.write("<h1>TraceZ Summary</h1>\n");
+
+ try {
+ emitHtmlBody(queryMap, out);
+ } catch (Throwable t) {
+ out.write("Errors while generate the HTML page " + t);
+ }
+ out.write("</body>\n");
+ out.write("</html>\n");
+ out.close();
+ }
+
+ private void emitHtmlBody(Map<String, String> queryMap, PrintWriter out)
+ throws UnsupportedEncodingException {
+ if (runningSpanStore == null || sampledSpanStore == null) {
+ out.write("OpenCensus implementation not available.");
+ return;
+ }
+ Formatter formatter = new Formatter(out, Locale.US);
+ emitSummaryTable(out, formatter);
+ String spanName = queryMap.get(HEADER_SPAN_NAME);
+ if (spanName != null) {
+ tracer
+ .getCurrentSpan()
+ .addAnnotation(
+ "Render spans.",
+ ImmutableMap.<String, AttributeValue>builder()
+ .put("SpanName", AttributeValue.stringAttributeValue(spanName))
+ .build());
+ String typeStr = queryMap.get(HEADER_SAMPLES_TYPE);
+ if (typeStr != null) {
+ List<SpanData> spans = null;
+ RequestType type = RequestType.fromString(typeStr);
+ if (type == RequestType.UNKNOWN) {
+ return;
+ }
+ if (type == RequestType.RUNNING) {
+ // Display running.
+ spans =
+ new ArrayList<>(
+ runningSpanStore.getRunningSpans(RunningSpanStore.Filter.create(spanName, 0)));
+ // Sort active spans incremental.
+ Collections.sort(spans, new SpanDataComparator(true));
+ } else {
+ String subtypeStr = queryMap.get(HEADER_SAMPLES_SUB_TYPE);
+ if (subtypeStr != null) {
+ int subtype = Integer.parseInt(subtypeStr);
+ if (type == RequestType.FAILED) {
+ if (subtype < 0 || subtype >= CanonicalCode.values().length) {
+ return;
+ }
+ // Display errors. subtype 0 means all.
+ CanonicalCode code = subtype == 0 ? null : CanonicalCode.values()[subtype];
+ spans =
+ new ArrayList<>(
+ sampledSpanStore.getErrorSampledSpans(ErrorFilter.create(spanName, code, 0)));
+ } else {
+ if (subtype < 0 || subtype >= LatencyBucketBoundaries.values().length) {
+ return;
+ }
+ // Display latency.
+ LatencyBucketBoundaries latencyBucketBoundaries =
+ LatencyBucketBoundaries.values()[subtype];
+ spans =
+ new ArrayList<>(
+ sampledSpanStore.getLatencySampledSpans(
+ LatencyFilter.create(
+ spanName,
+ latencyBucketBoundaries.getLatencyLowerNs(),
+ latencyBucketBoundaries.getLatencyUpperNs(),
+ 0)));
+ // Sort sampled spans decremental.
+ Collections.sort(spans, new SpanDataComparator(false));
+ }
+ }
+ }
+ emitSpanNameAndCountPages(formatter, spanName, spans == null ? 0 : spans.size(), type);
+
+ if (spans != null) {
+ emitSpans(out, formatter, spans);
+ emitLegend(out);
+ }
+ }
+ }
+ }
+
+ private static void emitSpanNameAndCountPages(
+ Formatter formatter, String spanName, int returnedNum, RequestType type) {
+ formatter.format("<p><b>Span Name: %s </b></p>%n", htmlEscaper().escape(spanName));
+ formatter.format(
+ "%s Requests %d</b></p>%n",
+ type == RequestType.RUNNING
+ ? "Running"
+ : type == RequestType.FINISHED ? "Finished" : "Failed",
+ returnedNum);
+ }
+
+ /** Emits the list of SampledRequets with a header. */
+ private static void emitSpans(PrintWriter out, Formatter formatter, Collection<SpanData> spans) {
+ out.write("<pre>\n");
+ formatter.format("%-23s %18s%n", "When", "Elapsed(s)");
+ out.write("-------------------------------------------\n");
+ for (SpanData span : spans) {
+ tracer
+ .getCurrentSpan()
+ .addAnnotation(
+ "Render span.",
+ ImmutableMap.<String, AttributeValue>builder()
+ .put(
+ "SpanId",
+ AttributeValue.stringAttributeValue(
+ BaseEncoding.base16()
+ .lowerCase()
+ .encode(span.getContext().getSpanId().getBytes())))
+ .build());
+
+ emitSingleSpan(out, formatter, span);
+ }
+ out.write("</pre>\n");
+ }
+
+ // Emits the internal html for a single {@link SpanData}.
+ @SuppressWarnings("deprecation")
+ private static void emitSingleSpan(PrintWriter out, Formatter formatter, SpanData span) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(TimeUnit.SECONDS.toMillis(span.getStartTimestamp().getSeconds()));
+ long microsField = TimeUnit.NANOSECONDS.toMicros(span.getStartTimestamp().getNanos());
+ String elapsedSecondsStr =
+ span.getEndTimestamp() != null
+ ? String.format(
+ "%13.6f",
+ durationToNanos(span.getEndTimestamp().subtractTimestamp(span.getStartTimestamp()))
+ * 1.0e-9)
+ : String.format("%13s", " ");
+
+ SpanContext spanContext = span.getContext();
+ formatter.format(
+ "<b>%04d/%02d/%02d-%02d:%02d:%02d.%06d %s TraceId: <b style=\"color:%s;\">%s</b> "
+ + "SpanId: %s ParentSpanId: %s</b>%n",
+ calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH) + 1,
+ calendar.get(Calendar.DAY_OF_MONTH),
+ calendar.get(Calendar.HOUR_OF_DAY),
+ calendar.get(Calendar.MINUTE),
+ calendar.get(Calendar.SECOND),
+ microsField,
+ elapsedSecondsStr,
+ spanContext.getTraceOptions().isSampled()
+ ? SAMPLED_TRACE_ID_COLOR
+ : NOT_SAMPLED_TRACE_ID_COLOR,
+ BaseEncoding.base16().lowerCase().encode(spanContext.getTraceId().getBytes()),
+ BaseEncoding.base16().lowerCase().encode(spanContext.getSpanId().getBytes()),
+ BaseEncoding.base16()
+ .lowerCase()
+ .encode(
+ span.getParentSpanId() == null
+ ? SpanId.INVALID.getBytes()
+ : span.getParentSpanId().getBytes()));
+
+ int lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
+
+ Timestamp lastTimestampNanos = span.getStartTimestamp();
+ TimedEvents<Annotation> annotations = span.getAnnotations();
+ TimedEvents<io.opencensus.trace.NetworkEvent> networkEvents = span.getNetworkEvents();
+ List<TimedEvent<?>> timedEvents = new ArrayList<TimedEvent<?>>(annotations.getEvents());
+ timedEvents.addAll(networkEvents.getEvents());
+ Collections.sort(timedEvents, new TimedEventComparator());
+ for (TimedEvent<?> event : timedEvents) {
+ // Special printing so that durations smaller than one second
+ // are left padded with blanks instead of '0' characters.
+ // E.g.,
+ // Number Printout
+ // ---------------------------------
+ // 0.000534 . 534
+ // 1.000534 1.000534
+ long deltaMicros =
+ TimeUnit.NANOSECONDS.toMicros(
+ durationToNanos(event.getTimestamp().subtractTimestamp(lastTimestampNanos)));
+ String deltaString;
+ if (deltaMicros >= 1000000) {
+ deltaString = String.format("%.6f", (deltaMicros / 1000000.0));
+ } else {
+ deltaString = String.format(".%6d", deltaMicros);
+ }
+
+ calendar.setTimeInMillis(
+ TimeUnit.SECONDS.toMillis(event.getTimestamp().getSeconds())
+ + TimeUnit.NANOSECONDS.toMillis(event.getTimestamp().getNanos()));
+ microsField = TimeUnit.NANOSECONDS.toMicros(event.getTimestamp().getNanos());
+
+ int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
+ if (dayOfYear == lastEntryDayOfYear) {
+ formatter.format("%11s", "");
+ } else {
+ formatter.format(
+ "%04d/%02d/%02d-",
+ calendar.get(Calendar.YEAR),
+ calendar.get(Calendar.MONTH) + 1,
+ calendar.get(Calendar.DAY_OF_MONTH));
+ lastEntryDayOfYear = dayOfYear;
+ }
+
+ formatter.format(
+ "%02d:%02d:%02d.%06d %13s ... %s%n",
+ calendar.get(Calendar.HOUR_OF_DAY),
+ calendar.get(Calendar.MINUTE),
+ calendar.get(Calendar.SECOND),
+ microsField,
+ deltaString,
+ htmlEscaper()
+ .escape(
+ event.getEvent() instanceof Annotation
+ ? renderAnnotation((Annotation) event.getEvent())
+ : renderNetworkEvents(
+ (io.opencensus.trace.NetworkEvent) castNonNull(event.getEvent()))));
+ lastTimestampNanos = event.getTimestamp();
+ }
+ Status status = span.getStatus();
+ if (status != null) {
+ formatter.format("%44s %s%n", "", htmlEscaper().escape(renderStatus(status)));
+ }
+ formatter.format(
+ "%44s %s%n",
+ "", htmlEscaper().escape(renderAttributes(span.getAttributes().getAttributeMap())));
+ }
+
+ // TODO(sebright): Remove this method.
+ @SuppressWarnings("nullness")
+ private static <T> T castNonNull(@javax.annotation.Nullable T arg) {
+ return arg;
+ }
+
+ // Emits the summary table with links to all samples.
+ private void emitSummaryTable(PrintWriter out, Formatter formatter)
+ throws UnsupportedEncodingException {
+ if (runningSpanStore == null || sampledSpanStore == null) {
+ return;
+ }
+ RunningSpanStore.Summary runningSpanStoreSummary = runningSpanStore.getSummary();
+ SampledSpanStore.Summary sampledSpanStoreSummary = sampledSpanStore.getSummary();
+
+ out.write("<table style='border-spacing: 0;\n");
+ out.write("border-left:1px solid #3D3D3D;border-right:1px solid #3D3D3D;'>\n");
+ emitSummaryTableHeader(out, formatter);
+
+ Set<String> spanNames = new TreeSet<>(runningSpanStoreSummary.getPerSpanNameSummary().keySet());
+ spanNames.addAll(sampledSpanStoreSummary.getPerSpanNameSummary().keySet());
+ boolean zebraColor = true;
+ for (String spanName : spanNames) {
+ out.write("<tr class=\"border\">\n");
+ if (!zebraColor) {
+ out.write("<tr class=\"border\">\n");
+ } else {
+ formatter.format("<tr class=\"border\" style=\"background: %s\">%n", ZEBRA_STRIPE_COLOR);
+ }
+ zebraColor = !zebraColor;
+ formatter.format("<td>%s</td>%n", htmlEscaper().escape(spanName));
+
+ // Running
+ out.write("<td class=\"borderRL\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ RunningSpanStore.PerSpanNameSummary runningSpanStorePerSpanNameSummary =
+ runningSpanStoreSummary.getPerSpanNameSummary().get(spanName);
+
+ // subtype ignored for running requests.
+ emitSingleCell(
+ out,
+ formatter,
+ spanName,
+ runningSpanStorePerSpanNameSummary == null
+ ? 0
+ : runningSpanStorePerSpanNameSummary.getNumRunningSpans(),
+ RequestType.RUNNING,
+ 0);
+
+ SampledSpanStore.PerSpanNameSummary sampledSpanStorePerSpanNameSummary =
+ sampledSpanStoreSummary.getPerSpanNameSummary().get(spanName);
+
+ // Latency based samples
+ out.write("<td class=\"borderLC\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ Map<LatencyBucketBoundaries, Integer> latencyBucketsSummaries =
+ sampledSpanStorePerSpanNameSummary != null
+ ? sampledSpanStorePerSpanNameSummary.getNumbersOfLatencySampledSpans()
+ : null;
+ int subtype = 0;
+ for (LatencyBucketBoundaries latencyBucketsBoundaries : LatencyBucketBoundaries.values()) {
+ if (latencyBucketsSummaries != null) {
+ int numSamples =
+ latencyBucketsSummaries.containsKey(latencyBucketsBoundaries)
+ ? latencyBucketsSummaries.get(latencyBucketsBoundaries)
+ : 0;
+ emitSingleCell(out, formatter, spanName, numSamples, RequestType.FINISHED, subtype++);
+ } else {
+ // numSamples < -1 means "Not Available".
+ emitSingleCell(out, formatter, spanName, -1, RequestType.FINISHED, subtype++);
+ }
+ }
+
+ // Error based samples.
+ out.write("<td class=\"borderRL\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ if (sampledSpanStorePerSpanNameSummary != null) {
+ Map<CanonicalCode, Integer> errorBucketsSummaries =
+ sampledSpanStorePerSpanNameSummary.getNumbersOfErrorSampledSpans();
+ int numErrorSamples = 0;
+ for (Map.Entry<CanonicalCode, Integer> it : errorBucketsSummaries.entrySet()) {
+ numErrorSamples += it.getValue();
+ }
+ // subtype 0 means all;
+ emitSingleCell(out, formatter, spanName, numErrorSamples, RequestType.FAILED, 0);
+ } else {
+ // numSamples < -1 means "Not Available".
+ emitSingleCell(out, formatter, spanName, -1, RequestType.FAILED, 0);
+ }
+
+ out.write("</tr>\n");
+ }
+ out.write("</table>");
+ }
+
+ private static void emitSummaryTableHeader(PrintWriter out, Formatter formatter) {
+ // First line.
+ out.write("<tr class=\"bgcolor\">\n");
+ out.write("<td colspan=1 class=\"head\"><b>Span Name</b></td>\n");
+ out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ out.write("<td colspan=1 class=\"head\"><b>Running</b></td>\n");
+ out.write("<td class=\"borderLW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ out.write("<td colspan=9 class=\"head\"><b>Latency Samples</b></td>\n");
+ out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ out.write("<td colspan=1 class=\"head\"><b>Error Samples</b></td>\n");
+ out.write("</tr>\n");
+ // Second line.
+ out.write("<tr class=\"bgcolor\">\n");
+ out.write("<td colspan=1></td>\n");
+ out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ out.write("<td colspan=1></td>\n");
+ out.write("<td class=\"borderLW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ for (LatencyBucketBoundaries latencyBucketsBoundaries : LatencyBucketBoundaries.values()) {
+ formatter.format(
+ "<td colspan=1 class=\"centerW\"><b>[%s]</b></td>%n",
+ LATENCY_BUCKET_BOUNDARIES_STRING_MAP.get(latencyBucketsBoundaries));
+ }
+ out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
+ out.write("<td colspan=1></td>\n");
+ out.write("</tr>\n");
+ }
+
+ // If numSamples is greater than 0 then emit a link to see span data, if the numSamples is
+ // negative then print "N/A", otherwise print the text "0".
+ private static void emitSingleCell(
+ PrintWriter out,
+ Formatter formatter,
+ String spanName,
+ int numSamples,
+ RequestType type,
+ int subtype)
+ throws UnsupportedEncodingException {
+ if (numSamples > 0) {
+ formatter.format(
+ "<td class=\"center\"><a href='?%s=%s&%s=%d&%s=%d'>%d</a></td>%n",
+ HEADER_SPAN_NAME,
+ URLEncoder.encode(spanName, "UTF-8"),
+ HEADER_SAMPLES_TYPE,
+ type.getValue(),
+ HEADER_SAMPLES_SUB_TYPE,
+ subtype,
+ numSamples);
+ } else if (numSamples < 0) {
+ out.write("<td class=\"center\">N/A</td>\n");
+ } else {
+ out.write("<td class=\"center\">0</td>\n");
+ }
+ }
+
+ private static void emitLegend(PrintWriter out) {
+ out.write("<br>\n");
+ out.printf(
+ "<p><b style=\"color:%s;\">TraceId</b> means sampled request. "
+ + "<b style=\"color:%s;\">TraceId</b> means not sampled request.</p>%n",
+ SAMPLED_TRACE_ID_COLOR, NOT_SAMPLED_TRACE_ID_COLOR);
+ }
+
+ private static Map<LatencyBucketBoundaries, String> buildLatencyBucketBoundariesStringMap() {
+ Map<LatencyBucketBoundaries, String> ret = new HashMap<>();
+ for (LatencyBucketBoundaries latencyBucketBoundaries : LatencyBucketBoundaries.values()) {
+ ret.put(latencyBucketBoundaries, latencyBucketBoundariesToString(latencyBucketBoundaries));
+ }
+ return Collections.unmodifiableMap(ret);
+ }
+
+ private static long durationToNanos(Duration duration) {
+ return TimeUnit.SECONDS.toNanos(duration.getSeconds()) + duration.getNanos();
+ }
+
+ private static String latencyBucketBoundariesToString(
+ LatencyBucketBoundaries latencyBucketBoundaries) {
+ switch (latencyBucketBoundaries) {
+ case ZERO_MICROSx10:
+ return ">0us";
+ case MICROSx10_MICROSx100:
+ return ">10us";
+ case MICROSx100_MILLIx1:
+ return ">100us";
+ case MILLIx1_MILLIx10:
+ return ">1ms";
+ case MILLIx10_MILLIx100:
+ return ">10ms";
+ case MILLIx100_SECONDx1:
+ return ">100ms";
+ case SECONDx1_SECONDx10:
+ return ">1s";
+ case SECONDx10_SECONDx100:
+ return ">10s";
+ case SECONDx100_MAX:
+ return ">100s";
+ }
+ throw new IllegalArgumentException("No value string available for: " + latencyBucketBoundaries);
+ }
+
+ @SuppressWarnings("deprecation")
+ private static String renderNetworkEvents(io.opencensus.trace.NetworkEvent networkEvent) {
+ StringBuilder stringBuilder = new StringBuilder();
+ if (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.RECV) {
+ stringBuilder.append("Received message");
+ } else if (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.SENT) {
+ stringBuilder.append("Sent message");
+ } else {
+ stringBuilder.append("Unknown");
+ }
+ stringBuilder.append(" id=");
+ stringBuilder.append(networkEvent.getMessageId());
+ stringBuilder.append(" uncompressed_size=");
+ stringBuilder.append(networkEvent.getUncompressedMessageSize());
+ stringBuilder.append(" compressed_size=");
+ stringBuilder.append(networkEvent.getCompressedMessageSize());
+ return stringBuilder.toString();
+ }
+
+ private static String renderAnnotation(Annotation annotation) {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append(annotation.getDescription());
+ if (!annotation.getAttributes().isEmpty()) {
+ stringBuilder.append(" ");
+ stringBuilder.append(renderAttributes(annotation.getAttributes()));
+ }
+ return stringBuilder.toString();
+ }
+
+ private static String renderStatus(Status status) {
+ return status.toString();
+ }
+
+ private static String renderAttributes(Map<String, AttributeValue> attributes) {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("Attributes:{");
+ boolean first = true;
+ for (Map.Entry<String, AttributeValue> entry : attributes.entrySet()) {
+ if (first) {
+ first = false;
+ stringBuilder.append(entry.getKey());
+ stringBuilder.append("=");
+ stringBuilder.append(attributeValueToString(entry.getValue()));
+ } else {
+ stringBuilder.append(", ");
+ stringBuilder.append(entry.getKey());
+ stringBuilder.append("=");
+ stringBuilder.append(attributeValueToString(entry.getValue()));
+ }
+ }
+ stringBuilder.append("}");
+ return stringBuilder.toString();
+ }
+
+ // The return type needs to be nullable when this function is used as an argument to 'match' in
+ // attributeValueToString, because 'match' doesn't allow covariant return types.
+ private static final Function<Object, /*@Nullable*/ String> returnToString =
+ Functions.returnToString();
+
+ @javax.annotation.Nullable
+ private static String attributeValueToString(AttributeValue attributeValue) {
+ return attributeValue.match(
+ returnToString,
+ returnToString,
+ returnToString,
+ returnToString,
+ Functions.</*@Nullable*/ String>returnNull());
+ }
+
+ private static final class TimedEventComparator
+ implements Comparator<TimedEvent<?>>, Serializable {
+ private static final long serialVersionUID = 0;
+
+ @Override
+ public int compare(TimedEvent<?> o1, TimedEvent<?> o2) {
+ return o1.getTimestamp().compareTo(o2.getTimestamp());
+ }
+ }
+
+ private static final class SpanDataComparator implements Comparator<SpanData>, Serializable {
+ private static final long serialVersionUID = 0;
+ private final boolean incremental;
+
+ /**
+ * Returns a new {@code SpanDataComparator}.
+ *
+ * @param incremental {@code true} if sorted incremental.
+ */
+ private SpanDataComparator(boolean incremental) {
+ this.incremental = incremental;
+ }
+
+ @Override
+ public int compare(SpanData o1, SpanData o2) {
+ return incremental
+ ? o1.getStartTimestamp().compareTo(o2.getStartTimestamp())
+ : o2.getStartTimestamp().compareTo(o1.getStartTimestamp());
+ }
+ }
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandler.java
new file mode 100644
index 00000000..172bca00
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandler.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.contrib.zpages;
+
+import java.io.OutputStream;
+import java.util.Map;
+
+/**
+ * Main interface for all the Z-Pages. All Z-Pages must implement this interface to allow other HTTP
+ * server implementation to support these pages.
+ *
+ * @since 0.6
+ */
+public abstract class ZPageHandler {
+
+ /**
+ * Returns the URL path that should be used to register this page.
+ *
+ * @return the URL path that should be used to register this page.
+ * @since 0.6
+ */
+ public abstract String getUrlPath();
+
+ /**
+ * Emits the HTML generated page to the {@code outputStream}.
+ *
+ * @param queryMap the query components map.
+ * @param outputStream the output {@code OutputStream}.
+ * @since 0.6
+ */
+ public abstract void emitHtml(Map<String, String> queryMap, OutputStream outputStream);
+
+ /** Package protected constructor to disallow users to extend this class. */
+ ZPageHandler() {}
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandlers.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandlers.java
new file mode 100644
index 00000000..710e9a20
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHandlers.java
@@ -0,0 +1,200 @@
+/*
+ * 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.contrib.zpages;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.sun.net.httpserver.HttpServer;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.trace.Tracing;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A collection of HTML pages to display stats and trace data and allow library configuration
+ * control.
+ *
+ * <p>Example usage with private {@link HttpServer}:
+ *
+ * <pre>{@code
+ * public class Main {
+ * public static void main(String[] args) throws Exception {
+ * ZPageHandlers.startHttpServerAndRegisterAll(8000);
+ * ... // do work
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Example usage with shared {@link HttpServer}:
+ *
+ * <pre>{@code
+ * public class Main {
+ * public static void main(String[] args) throws Exception {
+ * HttpServer server = HttpServer.create(new InetSocketAddress(8000), 10);
+ * ZPageHandlers.registerAllToHttpServer(server);
+ * server.start();
+ * ... // do work
+ * }
+ * }
+ * }</pre>
+ *
+ * @since 0.6
+ */
+@ThreadSafe
+public final class ZPageHandlers {
+ // The HttpServer listening socket backlog (maximum number of queued incoming connections).
+ private static final int BACKLOG = 5;
+ // How many seconds to wait for the HTTP server to stop.
+ private static final int STOP_DELAY = 1;
+ private static final Logger logger = Logger.getLogger(ZPageHandler.class.getName());
+ private static final ZPageHandler tracezZPageHandler =
+ TracezZPageHandler.create(
+ Tracing.getExportComponent().getRunningSpanStore(),
+ Tracing.getExportComponent().getSampledSpanStore());
+ private static final ZPageHandler traceConfigzZPageHandler =
+ TraceConfigzZPageHandler.create(Tracing.getTraceConfig());
+ private static final ZPageHandler rpczZpageHandler =
+ RpczZPageHandler.create(Stats.getViewManager());
+ private static final ZPageHandler statszZPageHandler =
+ StatszZPageHandler.create(Stats.getViewManager());
+
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static HttpServer server;
+
+ /**
+ * Returns a {@code ZPageHandler} for tracing debug. The page displays information about all
+ * active spans and all sampled spans based on latency and errors.
+ *
+ * <p>It prints a summary table which contains one row for each span name and data about number of
+ * active and sampled spans.
+ *
+ * <p>If no sampled spans based on latency and error codes are available for a given name, make
+ * sure that the span name is registered to the {@code SampledSpanStore}.
+ *
+ * @return a {@code ZPageHandler} for tracing debug.
+ * @since 0.6
+ */
+ public static ZPageHandler getTracezZPageHandler() {
+ return tracezZPageHandler;
+ }
+
+ /**
+ * Returns a {@code ZPageHandler} for tracing config. The page displays information about all
+ * active configuration and allow changing the active configuration.
+ *
+ * @return a {@code ZPageHandler} for tracing config.
+ * @since 0.6
+ */
+ public static ZPageHandler getTraceConfigzZPageHandler() {
+ return traceConfigzZPageHandler;
+ }
+
+ /**
+ * Returns a {@code ZPageHandler} for gRPC stats.
+ *
+ * <p>It prints a summary table which contains rows for each gRPC method.
+ *
+ * @return a {@code ZPageHandler} for gRPC stats.
+ * @since 0.12.0
+ */
+ public static ZPageHandler getRpczZpageHandler() {
+ return rpczZpageHandler;
+ }
+
+ /**
+ * Returns a {@code ZPageHandler} for all registered {@link View}s and {@link Measure}s.
+ *
+ * <p>Only {@code Cumulative} views are exported. {@link View}s are grouped by directories.
+ *
+ * @return a {@code ZPageHandler} for all registered {@code View}s and {@code Measure}s.
+ * @since 0.12.0
+ */
+ public static ZPageHandler getStatszZPageHandler() {
+ return statszZPageHandler;
+ }
+
+ /**
+ * Registers all pages to the given {@code HttpServer}.
+ *
+ * @param server the server that exports the tracez page.
+ * @since 0.6
+ */
+ public static void registerAllToHttpServer(HttpServer server) {
+ server.createContext(tracezZPageHandler.getUrlPath(), new ZPageHttpHandler(tracezZPageHandler));
+ server.createContext(
+ traceConfigzZPageHandler.getUrlPath(), new ZPageHttpHandler(traceConfigzZPageHandler));
+ server.createContext(rpczZpageHandler.getUrlPath(), new ZPageHttpHandler(rpczZpageHandler));
+ server.createContext(statszZPageHandler.getUrlPath(), new ZPageHttpHandler(statszZPageHandler));
+ }
+
+ /**
+ * Starts an {@code HttpServer} and registers all pages to it. When the JVM shuts down the server
+ * is stopped.
+ *
+ * <p>Users must call this function only once per process.
+ *
+ * @param port the port used to bind the {@code HttpServer}.
+ * @throws IllegalStateException if the server is already started.
+ * @throws IOException if the server cannot bind to the requested address.
+ * @since 0.6
+ */
+ public static void startHttpServerAndRegisterAll(int port) throws IOException {
+ synchronized (monitor) {
+ checkState(server == null, "The HttpServer is already started.");
+ server = HttpServer.create(new InetSocketAddress(port), BACKLOG);
+ ZPageHandlers.registerAllToHttpServer(server);
+ server.start();
+ logger.fine("HttpServer started on address " + server.getAddress().toString());
+ }
+
+ // This does not need to be mutex protected because it is guaranteed that only one thread will
+ // get ever here.
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread() {
+ @Override
+ public void run() {
+ // Use stderr here since the logger may have been reset by its JVM shutdown hook.
+ logger.fine("*** Shutting down gRPC server (JVM shutting down)");
+ ZPageHandlers.stop();
+ logger.fine("*** Server shut down");
+ }
+ });
+ }
+
+ private static void stop() {
+ synchronized (monitor) {
+ // This should never happen because we register the shutdown hook only if we start the server.
+ if (server == null) {
+ throw new IllegalStateException("The HttpServer is already stopped.");
+ }
+ server.stop(STOP_DELAY);
+ server = null;
+ }
+ }
+
+ private ZPageHandlers() {}
+}
diff --git a/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHttpHandler.java b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHttpHandler.java
new file mode 100644
index 00000000..975bdfcb
--- /dev/null
+++ b/contrib/zpages/src/main/java/io/opencensus/contrib/zpages/ZPageHttpHandler.java
@@ -0,0 +1,88 @@
+/*
+ * 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.contrib.zpages;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** An {@link HttpHandler} that can be used to render HTML pages using any {@code ZPageHandler}. */
+final class ZPageHttpHandler implements HttpHandler {
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final String HTTP_SERVER = "HttpServer";
+ private final ZPageHandler zpageHandler;
+ private final String httpServerSpanName;
+
+ /** Constructs a new {@code ZPageHttpHandler}. */
+ ZPageHttpHandler(ZPageHandler zpageHandler) {
+ this.zpageHandler = zpageHandler;
+ this.httpServerSpanName = HTTP_SERVER + zpageHandler.getUrlPath();
+ Tracing.getExportComponent()
+ .getSampledSpanStore()
+ .registerSpanNamesForCollection(Arrays.asList(httpServerSpanName));
+ }
+
+ @Override
+ public final void handle(HttpExchange httpExchange) throws IOException {
+ try (Scope ss =
+ tracer
+ .spanBuilderWithExplicitParent(httpServerSpanName, null)
+ .setRecordEvents(true)
+ .startScopedSpan()) {
+ tracer
+ .getCurrentSpan()
+ .putAttribute(
+ "/http/method ",
+ AttributeValue.stringAttributeValue(httpExchange.getRequestMethod()));
+ httpExchange.sendResponseHeaders(200, 0);
+ zpageHandler.emitHtml(
+ uriQueryToMap(httpExchange.getRequestURI()), httpExchange.getResponseBody());
+ } finally {
+ httpExchange.close();
+ }
+ }
+
+ @VisibleForTesting
+ static Map<String, String> uriQueryToMap(URI uri) {
+ String query = uri.getQuery();
+ if (query == null) {
+ return Collections.emptyMap();
+ }
+ Map<String, String> result = new HashMap<String, String>();
+ for (String param : Splitter.on("&").split(query)) {
+ List<String> splits = Splitter.on("=").splitToList(param);
+ if (splits.size() > 1) {
+ result.put(splits.get(0), splits.get(1));
+ } else {
+ result.put(splits.get(0), "");
+ }
+ }
+ return result;
+ }
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/RpczZPageHandlerTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/RpczZPageHandlerTest.java
new file mode 100644
index 00000000..2a75fe8b
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/RpczZPageHandlerTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2018, 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.contrib.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagValue;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link RpczZPageHandler}. */
+@RunWith(JUnit4.class)
+public class RpczZPageHandlerTest {
+
+ @Mock private final ViewManager mockViewManager = Mockito.mock(ViewManager.class);
+
+ private static final TagValue METHOD_1 = TagValue.create("method1");
+ private static final TagValue METHOD_2 = TagValue.create("method2");
+ private static final MeanData MEAN_DATA_1 = MeanData.create(5.5, 11);
+ private static final MeanData MEAN_DATA_2 = MeanData.create(1, 3);
+ private static final MeanData MEAN_DATA_3 = MeanData.create(1, 2);
+ private static final DistributionData DISTRIBUTION_DATA =
+ DistributionData.create(4.2, 5, 0.2, 16.3, 234.56, Arrays.asList(1L, 0L, 1L, 2L, 1L));
+ private static final CumulativeData CUMULATIVE_DATA =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(5000));
+ private static final IntervalData INTERVAL_DATA = IntervalData.create(Timestamp.fromMillis(8000));
+
+ @Test
+ public void getUrl() {
+ RpczZPageHandler handler = RpczZPageHandler.create(mockViewManager);
+ assertThat(handler.getUrlPath()).isEqualTo("/rpcz");
+ }
+
+ @Test
+ public void emitSummaryTableForEachMethod() {
+ doReturn(
+ ViewData.create(
+ RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW,
+ ImmutableMap.of(Arrays.asList(METHOD_1), MEAN_DATA_1),
+ INTERVAL_DATA))
+ .when(mockViewManager)
+ .getView(RPC_CLIENT_REQUEST_BYTES_MINUTE_VIEW.getName());
+ doReturn(
+ ViewData.create(
+ RPC_CLIENT_ERROR_COUNT_VIEW,
+ ImmutableMap.of(
+ Arrays.asList(METHOD_1), MEAN_DATA_2, Arrays.asList(METHOD_2), MEAN_DATA_3),
+ CUMULATIVE_DATA))
+ .when(mockViewManager)
+ .getView(RPC_CLIENT_ERROR_COUNT_VIEW.getName());
+ doReturn(
+ ViewData.create(
+ RPC_CLIENT_REQUEST_BYTES_VIEW,
+ ImmutableMap.of(Arrays.asList(METHOD_1), DISTRIBUTION_DATA),
+ CUMULATIVE_DATA))
+ .when(mockViewManager)
+ .getView(RPC_CLIENT_REQUEST_BYTES_VIEW.getName());
+ OutputStream output = new ByteArrayOutputStream();
+ RpczZPageHandler handler = RpczZPageHandler.create(mockViewManager);
+ handler.emitHtml(Maps.newHashMap(), output);
+ assertThat(output.toString()).contains(METHOD_1.asString());
+ assertThat(output.toString()).contains(METHOD_2.asString());
+ }
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/StatszZPageHandlerTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/StatszZPageHandlerTest.java
new file mode 100644
index 00000000..81e64a64
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/StatszZPageHandlerTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2018, 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.contrib.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_ERROR_COUNT;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_REQUEST_BYTES;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_CLIENT_ROUNDTRIP_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.RPC_SERVER_SERVER_LATENCY;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ERROR_COUNT_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_REQUEST_BYTES_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW;
+import static io.opencensus.contrib.grpc.metrics.RpcViewConstants.RPC_SERVER_SERVER_LATENCY_VIEW;
+import static io.opencensus.contrib.zpages.StatszZPageHandler.QUERY_PATH;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link StatszZPageHandler}. */
+@RunWith(JUnit4.class)
+public class StatszZPageHandlerTest {
+
+ @Mock private final ViewManager mockViewManager = Mockito.mock(ViewManager.class);
+
+ private static final View MY_VIEW =
+ View.create(
+ View.Name.create("my_view"),
+ "My view",
+ RPC_CLIENT_REQUEST_BYTES,
+ Sum.create(),
+ Arrays.asList(TagKey.create("my_key")),
+ Cumulative.create());
+ private static final TagValue METHOD_1 = TagValue.create("method1");
+ private static final TagValue METHOD_2 = TagValue.create("method2");
+ private static final TagValue METHOD_3 = TagValue.create("method3");
+ private static final AggregationData.MeanData MEAN_DATA = AggregationData.MeanData.create(1, 3);
+ private static final AggregationData.DistributionData DISTRIBUTION_DATA_1 =
+ AggregationData.DistributionData.create(
+ 4.2,
+ 5,
+ 0.2,
+ 16.3,
+ 234.56,
+ Arrays.asList(0L, 1L, 1L, 2L, 1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L));
+ private static final AggregationData.DistributionData DISTRIBUTION_DATA_2 =
+ AggregationData.DistributionData.create(
+ 7.9,
+ 11,
+ 5.1,
+ 12.2,
+ 123.88,
+ Arrays.asList(0L, 0L, 3L, 5L, 3L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L));
+ private static final ViewData.AggregationWindowData.CumulativeData CUMULATIVE_DATA =
+ ViewData.AggregationWindowData.CumulativeData.create(
+ Timestamp.fromMillis(1000), Timestamp.fromMillis(5000));
+ private static final ViewData VIEW_DATA_1 =
+ ViewData.create(
+ RPC_CLIENT_REQUEST_BYTES_VIEW,
+ ImmutableMap.of(
+ Arrays.asList(METHOD_1), DISTRIBUTION_DATA_1,
+ Arrays.asList(METHOD_2), DISTRIBUTION_DATA_2),
+ CUMULATIVE_DATA);
+ private static final ViewData VIEW_DATA_2 =
+ ViewData.create(
+ RPC_CLIENT_ERROR_COUNT_VIEW,
+ ImmutableMap.of(Arrays.asList(METHOD_3), MEAN_DATA),
+ CUMULATIVE_DATA);
+
+ @Before
+ public void setUp() {
+ doReturn(
+ ImmutableSet.of(
+ RPC_CLIENT_REQUEST_BYTES_VIEW,
+ RPC_CLIENT_ERROR_COUNT_VIEW,
+ RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW,
+ RPC_SERVER_SERVER_LATENCY_VIEW,
+ MY_VIEW))
+ .when(mockViewManager)
+ .getAllExportedViews();
+ doReturn(VIEW_DATA_1)
+ .when(mockViewManager)
+ .getView(RPC_CLIENT_ROUNDTRIP_LATENCY_VIEW.getName());
+ doReturn(VIEW_DATA_2).when(mockViewManager).getView(RPC_CLIENT_ERROR_COUNT_VIEW.getName());
+ }
+
+ @Test
+ public void getUrl() {
+ StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+ assertThat(handler.getUrlPath()).isEqualTo("/statsz");
+ }
+
+ @Test
+ public void emitMeasures() {
+ OutputStream output = new ByteArrayOutputStream();
+ StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+ handler.emitHtml(Maps.newHashMap(), output);
+ assertContainsMeasure(output, RPC_CLIENT_REQUEST_BYTES);
+ assertContainsMeasure(output, RPC_CLIENT_ERROR_COUNT);
+ assertContainsMeasure(output, RPC_CLIENT_ROUNDTRIP_LATENCY);
+ assertContainsMeasure(output, RPC_SERVER_SERVER_LATENCY);
+ }
+
+ @Test
+ public void emitDirectoriesAndViews() {
+ StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+
+ OutputStream output1 = new ByteArrayOutputStream();
+ handler.emitHtml(Maps.newHashMap(), output1);
+ assertThat(output1.toString()).contains("grpc.io");
+ assertThat(output1.toString()).contains("(4 views)");
+ assertThat(output1.toString()).contains("my_view");
+
+ OutputStream output2 = new ByteArrayOutputStream();
+ handler.emitHtml(ImmutableMap.of(QUERY_PATH, "/grpc.io"), output2);
+ assertThat(output2.toString()).contains("client");
+ assertThat(output2.toString()).contains("(3 views)");
+ assertThat(output2.toString()).contains("server");
+ assertThat(output2.toString()).contains("(1 view)");
+
+ OutputStream output3 = new ByteArrayOutputStream();
+ handler.emitHtml(ImmutableMap.of(QUERY_PATH, "/grpc.io/client"), output3);
+ assertThat(output3.toString()).contains("request_bytes");
+ assertThat(output3.toString()).contains("error_count");
+ assertThat(output3.toString()).contains("roundtrip_latency");
+ assertThat(output3.toString()).contains("(1 view)");
+ }
+
+ @Test
+ public void emitViewData() {
+ StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+
+ OutputStream output1 = new ByteArrayOutputStream();
+ handler.emitHtml(
+ ImmutableMap.of(QUERY_PATH, "/grpc.io/client/roundtrip_latency/cumulative"), output1);
+ assertContainsViewData(output1, VIEW_DATA_1);
+
+ OutputStream output2 = new ByteArrayOutputStream();
+ handler.emitHtml(
+ ImmutableMap.of(QUERY_PATH, "/grpc.io/client/error_count/cumulative"), output2);
+ assertContainsViewData(output2, VIEW_DATA_2);
+ }
+
+ @Test
+ public void nonExistingPath() {
+ StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+ OutputStream output = new ByteArrayOutputStream();
+ handler.emitHtml(ImmutableMap.of(QUERY_PATH, "/unknown/unknown_view"), output);
+ assertThat(output.toString())
+ .contains("Directory not found: /unknown/unknown_view. Return to root.");
+ }
+
+ @Test
+ public void viewWithNoStats() {
+ StatszZPageHandler handler = StatszZPageHandler.create(mockViewManager);
+ OutputStream output = new ByteArrayOutputStream();
+ handler.emitHtml(ImmutableMap.of(QUERY_PATH, "/my_view"), output);
+ assertThat(output.toString()).contains("No Stats found for View my_view.");
+ }
+
+ private static void assertContainsMeasure(OutputStream output, Measure measure) {
+ assertThat(output.toString()).contains(measure.getName());
+ assertThat(output.toString()).contains(measure.getDescription());
+ assertThat(output.toString()).contains(measure.getUnit());
+ String type =
+ measure.match(
+ Functions.returnConstant("Double"),
+ Functions.returnConstant("Long"),
+ Functions.throwAssertionError());
+ assertThat(output.toString()).contains(type);
+ }
+
+ private static void assertContainsViewData(OutputStream output, ViewData viewData) {
+ View view = viewData.getView();
+ assertThat(output.toString()).contains(view.getName().asString());
+ assertThat(output.toString()).contains(view.getDescription());
+ assertThat(output.toString()).contains(view.getMeasure().getName());
+ for (TagKey tagKey : view.getColumns()) {
+ assertThat(output.toString()).contains(tagKey.getName());
+ }
+ String aggregationType =
+ view.getAggregation()
+ .match(
+ Functions.returnConstant("Sum"),
+ Functions.returnConstant("Count"),
+ Functions.returnConstant("Distribution"),
+ Functions.returnConstant("Last Value"),
+ new Function<Aggregation, String>() {
+ @Override
+ public String apply(Aggregation arg) {
+ if (arg instanceof Aggregation.Mean) {
+ return "Mean";
+ }
+ throw new AssertionError();
+ }
+ });
+ assertThat(output.toString()).contains(aggregationType);
+ for (Map.Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ viewData.getAggregationMap().entrySet()) {
+ List<TagValue> tagValues = entry.getKey();
+ for (TagValue tagValue : tagValues) {
+ String tagValueStr = tagValue == null ? "" : tagValue.asString();
+ assertThat(output.toString()).contains(tagValueStr);
+ }
+ entry
+ .getValue()
+ .match(
+ Functions.</*@Nullable*/ Void>throwAssertionError(),
+ Functions.</*@Nullable*/ Void>throwAssertionError(),
+ Functions.</*@Nullable*/ Void>throwAssertionError(),
+ new Function<AggregationData.DistributionData, Void>() {
+ @Override
+ public Void apply(AggregationData.DistributionData arg) {
+ assertThat(output.toString()).contains(String.valueOf(arg.getCount()));
+ assertThat(output.toString()).contains(String.valueOf(arg.getMax()));
+ assertThat(output.toString()).contains(String.valueOf(arg.getMin()));
+ assertThat(output.toString()).contains(String.valueOf(arg.getMean()));
+ assertThat(output.toString())
+ .contains(String.valueOf(arg.getSumOfSquaredDeviations()));
+ return null;
+ }
+ },
+ Functions.</*@Nullable*/ Void>throwAssertionError(),
+ Functions.</*@Nullable*/ Void>throwAssertionError(),
+ new Function<AggregationData, Void>() {
+ @Override
+ public Void apply(AggregationData arg) {
+ if (arg instanceof MeanData) {
+ MeanData meanData = (MeanData) arg;
+ assertThat(output.toString()).contains(String.valueOf(meanData.getCount()));
+ assertThat(output.toString()).contains(String.valueOf(meanData.getMean()));
+ return null;
+ }
+ throw new AssertionError();
+ }
+ });
+ }
+ }
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/TracezZPageHandlerTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/TracezZPageHandlerTest.java
new file mode 100644
index 00000000..63ea8c45
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/TracezZPageHandlerTest.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.contrib.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SampledSpanStore;
+import io.opencensus.trace.export.SampledSpanStore.LatencyBucketBoundaries;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link TracezZPageHandler}. */
+@RunWith(JUnit4.class)
+public class TracezZPageHandlerTest {
+ private static final String ACTIVE_SPAN_NAME = "TestActiveSpan";
+ private static final String SAMPLED_SPAN_NAME = "TestSampledSpan";
+ private static final String ACTIVE_SAMPLED_SPAN_NAME = "TestActiveAndSampledSpan";
+ @Mock private RunningSpanStore runningSpanStore;
+ @Mock private SampledSpanStore sampledSpanStore;
+ RunningSpanStore.Summary runningSpanStoreSummary;
+ SampledSpanStore.Summary sampledSpanStoreSummary;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Map<String, RunningSpanStore.PerSpanNameSummary> runningSummaryMap = new HashMap<>();
+ runningSummaryMap.put(ACTIVE_SPAN_NAME, RunningSpanStore.PerSpanNameSummary.create(3));
+ runningSummaryMap.put(ACTIVE_SAMPLED_SPAN_NAME, RunningSpanStore.PerSpanNameSummary.create(5));
+ runningSpanStoreSummary = RunningSpanStore.Summary.create(runningSummaryMap);
+ Map<LatencyBucketBoundaries, Integer> numbersOfLatencySampledSpans = new HashMap<>();
+ numbersOfLatencySampledSpans.put(LatencyBucketBoundaries.MILLIx1_MILLIx10, 3);
+ numbersOfLatencySampledSpans.put(LatencyBucketBoundaries.MICROSx10_MICROSx100, 7);
+ Map<CanonicalCode, Integer> numbersOfErrorSampledSpans = new HashMap<>();
+ numbersOfErrorSampledSpans.put(CanonicalCode.CANCELLED, 2);
+ numbersOfErrorSampledSpans.put(CanonicalCode.DEADLINE_EXCEEDED, 5);
+ Map<String, SampledSpanStore.PerSpanNameSummary> sampledSummaryMap = new HashMap<>();
+ sampledSummaryMap.put(
+ SAMPLED_SPAN_NAME,
+ SampledSpanStore.PerSpanNameSummary.create(
+ numbersOfLatencySampledSpans, numbersOfErrorSampledSpans));
+ sampledSummaryMap.put(
+ ACTIVE_SAMPLED_SPAN_NAME,
+ SampledSpanStore.PerSpanNameSummary.create(
+ numbersOfLatencySampledSpans, numbersOfErrorSampledSpans));
+ sampledSpanStoreSummary = SampledSpanStore.Summary.create(sampledSummaryMap);
+ }
+
+ @Test
+ public void emitSummaryTableForEachSpan() {
+ OutputStream output = new ByteArrayOutputStream();
+ TracezZPageHandler tracezZPageHandler =
+ TracezZPageHandler.create(runningSpanStore, sampledSpanStore);
+ when(runningSpanStore.getSummary()).thenReturn(runningSpanStoreSummary);
+ when(sampledSpanStore.getSummary()).thenReturn(sampledSpanStoreSummary);
+ tracezZPageHandler.emitHtml(Collections.emptyMap(), output);
+ assertThat(output.toString()).contains(ACTIVE_SPAN_NAME);
+ assertThat(output.toString()).contains(SAMPLED_SPAN_NAME);
+ assertThat(output.toString()).contains(ACTIVE_SAMPLED_SPAN_NAME);
+ }
+
+ @Test
+ public void linksForActiveRequests_InSummaryTable() {
+ OutputStream output = new ByteArrayOutputStream();
+ TracezZPageHandler tracezZPageHandler =
+ TracezZPageHandler.create(runningSpanStore, sampledSpanStore);
+ when(runningSpanStore.getSummary()).thenReturn(runningSpanStoreSummary);
+ when(sampledSpanStore.getSummary()).thenReturn(sampledSpanStoreSummary);
+ tracezZPageHandler.emitHtml(Collections.emptyMap(), output);
+ // 3 active requests
+ assertThat(output.toString()).contains("href='?zspanname=TestActiveSpan&ztype=0&zsubtype=0'>3");
+ // No active links
+ assertThat(output.toString())
+ .doesNotContain("href='?zspanname=TestSampledSpan&ztype=0&zsubtype=0'");
+ // 5 active requests
+ assertThat(output.toString())
+ .contains("href='?zspanname=TestActiveAndSampledSpan&ztype=0&zsubtype=0'>5");
+ }
+
+ @Test
+ public void linksForSampledRequests_InSummaryTable() {
+ OutputStream output = new ByteArrayOutputStream();
+ TracezZPageHandler tracezZPageHandler =
+ TracezZPageHandler.create(runningSpanStore, sampledSpanStore);
+ when(runningSpanStore.getSummary()).thenReturn(runningSpanStoreSummary);
+ when(sampledSpanStore.getSummary()).thenReturn(sampledSpanStoreSummary);
+ tracezZPageHandler.emitHtml(Collections.emptyMap(), output);
+ // No sampled links (ztype=1);
+ assertThat(output.toString()).doesNotContain("href=\"?zspanname=TestActiveSpan&ztype=1");
+ // Links for 7 samples [10us, 100us) and 3 samples [1ms, 10ms);
+ assertThat(output.toString())
+ .contains("href='?zspanname=TestSampledSpan&ztype=1&zsubtype=1'>7");
+ assertThat(output.toString())
+ .contains("href='?zspanname=TestSampledSpan&ztype=1&zsubtype=3'>3");
+ // Links for 7 samples [10us, 100us) and 3 samples [1ms, 10ms);
+ assertThat(output.toString())
+ .contains("href='?zspanname=TestActiveAndSampledSpan&ztype=1&zsubtype=1'>7");
+ assertThat(output.toString())
+ .contains("href='?zspanname=TestActiveAndSampledSpan&ztype=1&zsubtype=3'>3");
+ }
+
+ @Test
+ public void linksForFailedRequests_InSummaryTable() {
+ OutputStream output = new ByteArrayOutputStream();
+ TracezZPageHandler tracezZPageHandler =
+ TracezZPageHandler.create(runningSpanStore, sampledSpanStore);
+ when(runningSpanStore.getSummary()).thenReturn(runningSpanStoreSummary);
+ when(sampledSpanStore.getSummary()).thenReturn(sampledSpanStoreSummary);
+ tracezZPageHandler.emitHtml(Collections.emptyMap(), output);
+ // No sampled links (ztype=1);
+ assertThat(output.toString()).doesNotContain("href=\"?zspanname=TestActiveSpan&ztype=2");
+ // Links for 7 errors 2 CANCELLED + 5 DEADLINE_EXCEEDED;
+ assertThat(output.toString())
+ .contains("href='?zspanname=TestSampledSpan&ztype=2&zsubtype=0'>7");
+ // Links for 7 errors 2 CANCELLED + 5 DEADLINE_EXCEEDED;
+ assertThat(output.toString())
+ .contains("href='?zspanname=TestActiveAndSampledSpan&ztype=2&zsubtype=0'>7");
+ }
+
+ // TODO(bdrutu): Add tests for latency.
+ // TODO(bdrutu): Add tests for samples/running/errors.
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHandlersTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHandlersTest.java
new file mode 100644
index 00000000..a7bbf11c
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHandlersTest.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.contrib.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ZPageHandlers}. */
+@RunWith(JUnit4.class)
+public class ZPageHandlersTest {
+
+ @Test
+ public void implementationOfTracez() {
+ assertThat(ZPageHandlers.getTracezZPageHandler()).isInstanceOf(TracezZPageHandler.class);
+ }
+
+ @Test
+ public void implementationOfTraceConfigz() {
+ assertThat(ZPageHandlers.getTraceConfigzZPageHandler())
+ .isInstanceOf(TraceConfigzZPageHandler.class);
+ }
+
+ @Test
+ public void implementationOfRpcz() {
+ assertThat(ZPageHandlers.getRpczZpageHandler()).isInstanceOf(RpczZPageHandler.class);
+ }
+
+ @Test
+ public void implementationOfStatsz() {
+ assertThat(ZPageHandlers.getStatszZPageHandler()).isInstanceOf(StatszZPageHandler.class);
+ }
+}
diff --git a/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHttpHandlerTest.java b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHttpHandlerTest.java
new file mode 100644
index 00000000..7ac5ba65
--- /dev/null
+++ b/contrib/zpages/src/test/java/io/opencensus/contrib/zpages/ZPageHttpHandlerTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.contrib.zpages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ZPageHttpHandler}. */
+@RunWith(JUnit4.class)
+public class ZPageHttpHandlerTest {
+ @Test
+ public void parseUndefinedQuery() throws URISyntaxException {
+ URI uri = new URI("http://localhost:8000/tracez");
+ assertThat(ZPageHttpHandler.uriQueryToMap(uri)).isEmpty();
+ }
+
+ @Test
+ public void parseQuery() throws URISyntaxException {
+ URI uri = new URI("http://localhost:8000/tracez?ztype=1&zsubtype&zname=Test");
+ assertThat(ZPageHttpHandler.uriQueryToMap(uri))
+ .containsExactly("ztype", "1", "zsubtype", "", "zname", "Test");
+ }
+}
diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel
new file mode 100644
index 00000000..a93cefd0
--- /dev/null
+++ b/examples/BUILD.bazel
@@ -0,0 +1,155 @@
+load("//:opencensus_workspace.bzl", "opencensus_java_libraries")
+load("@grpc_java//:java_grpc_library.bzl", "java_grpc_library")
+
+opencensus_java_libraries()
+
+proto_library(
+ name = "helloworld_proto",
+ srcs = ["src/main/proto/helloworld.proto"],
+)
+
+java_proto_library(
+ name = "helloworld_java_proto",
+ deps = [":helloworld_proto"],
+)
+
+java_grpc_library(
+ name = "helloworld_java_grpc",
+ srcs = [":helloworld_proto"],
+ deps = [":helloworld_java_proto"],
+)
+
+java_library(
+ name = "opencensus_examples",
+ srcs = glob(
+ ["src/main/java/**/*.java"],
+ ),
+ deps = [
+ ":helloworld_java_grpc",
+ ":helloworld_java_proto",
+ "@com_google_guava_guava//jar",
+ "@com_google_code_findbugs_jsr305//jar",
+ "@io_opencensus_opencensus_api//jar",
+ "@io_opencensus_opencensus_contrib_grpc_metrics//jar",
+ "@io_opencensus_opencensus_contrib_zpages//jar",
+ "@io_opencensus_opencensus_exporter_stats_prometheus//jar",
+ "@io_opencensus_opencensus_exporter_stats_stackdriver//jar",
+ "@io_opencensus_opencensus_exporter_trace_logging//jar",
+ "@io_opencensus_opencensus_exporter_trace_stackdriver//jar",
+ "@io_grpc_grpc_core//jar",
+ "@io_grpc_grpc_netty//jar",
+ "@io_grpc_grpc_protobuf//jar",
+ "@io_grpc_grpc_stub//jar",
+ "@io_prometheus_simpleclient//jar",
+ "@io_prometheus_simpleclient_httpserver//jar",
+ ],
+ runtime_deps = [
+ "@com_google_api_api_common//jar",
+ "@com_google_api_gax//jar",
+ "@com_google_api_gax_grpc//jar",
+ "@com_google_api_grpc_proto_google_cloud_trace_v1//jar",
+ "@com_google_api_grpc_proto_google_cloud_trace_v2//jar",
+ "@com_google_api_grpc_proto_google_iam_v1//jar",
+ "@com_google_api_grpc_proto_google_cloud_monitoring_v3//jar",
+ "@com_google_api_grpc_proto_google_common_protos//jar",
+ "@com_google_auth_google_auth_library_credentials//jar",
+ "@com_google_auth_google_auth_library_oauth2_http//jar",
+ "@com_google_cloud_google_cloud_core//jar",
+ "@com_google_cloud_google_cloud_core_grpc//jar",
+ "@com_google_cloud_google_cloud_monitoring//jar",
+ "@com_google_cloud_google_cloud_trace//jar",
+ "@com_google_http_client_google_http_client//jar",
+ "@com_google_http_client_google_http_client_jackson2//jar",
+ "@com_google_instrumentation_instrumentation_api//jar",
+ "@com_google_protobuf_protobuf_java//jar",
+ "@com_google_protobuf_protobuf_java_util//jar",
+ "@commons_codec_commons_codec//jar",
+ "@commons_logging_commons_logging//jar",
+
+ "@com_lmax_disruptor//jar",
+ "@io_grpc_grpc_context//jar",
+ "@io_grpc_grpc_auth//jar",
+ "@io_grpc_grpc_protobuf_lite//jar",
+ "@io_netty_netty_buffer//jar",
+ "@io_netty_netty_common//jar",
+ "@io_netty_netty_codec//jar",
+ "@io_netty_netty_codec_socks//jar",
+ "@io_netty_netty_codec_http//jar",
+ "@io_netty_netty_codec_http2//jar",
+ "@io_netty_netty_handler//jar",
+ "@io_netty_netty_handler_proxy//jar",
+ "@io_netty_netty_resolver//jar",
+ "@io_netty_netty_tcnative_boringssl_static//jar",
+ "@io_netty_netty_transport//jar",
+ "@io_opencensus_opencensus_impl//jar",
+ "@io_opencensus_opencensus_impl_core//jar",
+ "@joda_time_joda_time//jar",
+ "@org_apache_httpcomponents_httpclient//jar",
+ "@org_apache_httpcomponents_httpcore//jar",
+ "@org_threeten_threetenbp//jar",
+ ],
+)
+
+java_binary(
+ name = "TagContextExample",
+ main_class = "io.opencensus.examples.tags.TagContextExample",
+ runtime_deps = [
+ ":opencensus_examples",
+ ],
+)
+
+java_binary(
+ name = "MultiSpansTracing",
+ main_class = "io.opencensus.examples.trace.MultiSpansTracing",
+ runtime_deps = [
+ ":opencensus_examples",
+ ],
+)
+
+java_binary(
+ name = "MultiSpansScopedTracing",
+ main_class = "io.opencensus.examples.trace.MultiSpansScopedTracing",
+ runtime_deps = [
+ ":opencensus_examples",
+ ],
+)
+
+java_binary(
+ name = "MultiSpansContextTracing",
+ main_class = "io.opencensus.examples.trace.MultiSpansContextTracing",
+ runtime_deps = [
+ ":opencensus_examples",
+ ],
+)
+
+java_binary(
+ name = "ZPagesTester",
+ main_class = "io.opencensus.examples.zpages.ZPagesTester",
+ runtime_deps = [
+ ":opencensus_examples",
+ ],
+)
+
+java_binary(
+ name = "QuickStart",
+ main_class = "io.opencensus.examples.helloworld.QuickStart",
+ runtime_deps = [
+ ":opencensus_examples",
+ ],
+)
+
+java_binary(
+ name = "HelloWorldClient",
+ main_class = "io.opencensus.examples.grpc.helloworld.HelloWorldClient",
+ runtime_deps = [
+ ":opencensus_examples",
+ ],
+)
+
+java_binary(
+ name = "HelloWorldServer",
+ main_class = "io.opencensus.examples.grpc.helloworld.HelloWorldServer",
+ runtime_deps = [
+ ":opencensus_examples",
+ ],
+)
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..921691b7
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,113 @@
+# OpenCensus Examples
+
+## To build the examples use
+
+### Gradle
+```
+$ ./gradlew installDist
+```
+
+### Maven
+```
+$ mvn package appassembler:assemble
+```
+
+### Bazel
+```
+$ bazel build :all
+```
+
+## To run "TagContextExample" use
+
+### Gradle
+```
+$ ./build/install/opencensus-examples/bin/TagContextExample
+```
+
+### Maven
+```
+$ ./target/appassembler/bin/TagContextExample
+```
+
+### Bazel
+```
+$ ./bazel-bin/TagContextExample
+```
+
+## To run "ZPagesTester"
+
+### Gradle
+```
+$ ./build/install/opencensus-examples/bin/ZPagesTester
+```
+
+### Maven
+```
+$ ./target/appassembler/bin/ZPagesTester
+```
+
+### Bazel
+```
+$ ./bazel-bin/ZPagesTester
+```
+
+Available pages:
+* For tracing page go to [localhost:8080/tracez][ZPagesTraceZLink].
+* For tracing config page go to [localhost:8080/traceconfigz][ZPagesTraceConfigZLink].
+* For RPC stats page go to [localhost:8080/rpcz][ZPagesRpcZLink].
+* For stats and measures on all registered views go to [localhost:8080/statsz][ZPagesStatsZLink].
+
+[ZPagesTraceZLink]: http://localhost:8080/tracez
+[ZPagesTraceConfigZLink]: http://localhost:8080/traceconfigz
+[ZPagesRpcZLink]: http://localhost:8080/rpcz
+[ZPagesStatsZLink]: http://localhost:8080/statsz
+
+## To run "QuickStart" example use
+
+### Gradle
+```
+$ ./build/install/opencensus-examples/bin/QuickStart
+```
+
+### Maven
+```
+$ ./target/appassembler/bin/QuickStart
+```
+
+### Bazel
+```
+$ ./bazel-bin/QuickStart
+```
+
+## To run "gRPC Hello World" example use
+
+Please note all the arguments are optional. If you do not specify these arguments, default values
+will be used:
+
+* host and serverPort will be "localhost:50051"
+* user will be "world"
+* cloudProjectId will be null (which means no stats/spans will be exported to Stackdriver)
+* server zPagePort will be 3000
+* client zPagePort will be 3001
+* Prometheus port will be 9090
+
+
+However, if you want to specify any of these arguements, please make sure they are in order.
+
+### Gradle
+```
+$ ./build/install/opencensus-examples/bin/HelloWorldServer serverPort cloudProjectId zPagePort prometheusPort
+$ ./build/install/opencensus-examples/bin/HelloWorldClient user host serverPort cloudProjectId zPagePort
+```
+
+### Maven
+```
+$ ./target/appassembler/bin/HelloWorldServer serverPort cloudProjectId zPagePort prometheusPort
+$ ./target/appassembler/bin/HelloWorldClient user host serverPort cloudProjectId zPagePort
+```
+
+### Bazel
+```
+$ ./bazel-bin/HelloWorldServer serverPort cloudProjectId zPagePort prometheusPort
+$ ./bazel-bin/HelloWorldClient user host serverPort cloudProjectId zPagePort
+```
diff --git a/examples/WORKSPACE b/examples/WORKSPACE
new file mode 100644
index 00000000..a065f962
--- /dev/null
+++ b/examples/WORKSPACE
@@ -0,0 +1,53 @@
+workspace(name = "opencensus_examples")
+
+git_repository(
+ name = "grpc_java",
+ remote = "https://github.com/grpc/grpc-java.git",
+ tag = "v1.10.1",
+)
+
+load("//:opencensus_workspace.bzl", "opencensus_maven_jars")
+load("@grpc_java//:repositories.bzl", "grpc_java_repositories")
+
+opencensus_maven_jars()
+grpc_java_repositories(
+ # Omit to avoid conflicts.
+
+ omit_com_google_auth_google_auth_library_credentials=True,
+ omit_com_google_api_grpc_google_common_protos=True,
+ omit_com_google_code_findbugs_jsr305=True,
+ omit_com_google_code_gson=True,
+ omit_com_google_errorprone_error_prone_annotations=True,
+ omit_com_google_guava=True,
+ omit_com_google_protobuf=True,
+ omit_com_google_protobuf_nano_protobuf_javanano=True,
+ omit_com_google_truth_truth=True,
+ omit_com_squareup_okhttp=True,
+ omit_com_squareup_okio=True,
+
+ # These netty dependencies have already been included in opencensus_workspace.bzl
+ omit_io_netty_buffer=True,
+ omit_io_netty_common=True,
+ omit_io_netty_handler_proxy=True,
+ omit_io_netty_codec_http2=True,
+ omit_io_netty_transport=True,
+ omit_io_netty_codec=True,
+ omit_io_netty_codec_socks=True,
+ omit_io_netty_codec_http=True,
+ omit_io_netty_handler=True,
+ omit_io_netty_resolver=True,
+
+ omit_io_opencensus_api=True,
+ omit_io_opencensus_grpc_metrics=True,
+ omit_junit_junit=True
+)
+
+# proto_library, cc_proto_library, and java_proto_library rules implicitly
+# depend on @com_google_protobuf for protoc and proto runtimes.
+# This statement defines the @com_google_protobuf repo.
+http_archive(
+ name = "com_google_protobuf",
+ sha256 = "1f8b9b202e9a4e467ff0b0f25facb1642727cdf5e69092038f15b37c75b99e45",
+ strip_prefix = "protobuf-3.5.1",
+ urls = ["https://github.com/google/protobuf/archive/v3.5.1.zip"],
+)
diff --git a/examples/build.gradle b/examples/build.gradle
new file mode 100644
index 00000000..22889e10
--- /dev/null
+++ b/examples/build.gradle
@@ -0,0 +1,154 @@
+description = 'OpenCensus Examples'
+
+buildscript {
+ repositories {
+ mavenCentral()
+ mavenLocal()
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.3'
+ }
+}
+
+apply plugin: 'idea'
+apply plugin: 'java'
+apply plugin: 'com.google.protobuf'
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+}
+
+group = "io.opencensus"
+version = "0.17.0-SNAPSHOT" // CURRENT_OPENCENSUS_VERSION
+
+def opencensusVersion = "0.16.1" // LATEST_OPENCENSUS_RELEASE_VERSION
+def grpcVersion = "1.13.1" // CURRENT_GRPC_VERSION
+def prometheusVersion = "0.3.0"
+
+tasks.withType(JavaCompile) {
+ sourceCompatibility = '1.8'
+ targetCompatibility = '1.8'
+}
+
+dependencies {
+ compile "com.google.api.grpc:proto-google-common-protos:1.11.0",
+ "io.opencensus:opencensus-api:${opencensusVersion}",
+ "io.opencensus:opencensus-contrib-zpages:${opencensusVersion}",
+ "io.opencensus:opencensus-contrib-grpc-metrics:${opencensusVersion}",
+ "io.opencensus:opencensus-exporter-stats-prometheus:${opencensusVersion}",
+ "io.opencensus:opencensus-exporter-stats-stackdriver:${opencensusVersion}",
+ "io.opencensus:opencensus-exporter-trace-stackdriver:${opencensusVersion}",
+ "io.opencensus:opencensus-exporter-trace-logging:${opencensusVersion}",
+ "io.grpc:grpc-protobuf:${grpcVersion}",
+ "io.grpc:grpc-stub:${grpcVersion}",
+ "io.grpc:grpc-netty:${grpcVersion}",
+ "io.prometheus:simpleclient_httpserver:${prometheusVersion}"
+
+ runtime "io.opencensus:opencensus-impl:${opencensusVersion}",
+ "io.netty:netty-tcnative-boringssl-static:2.0.8.Final"
+}
+
+protobuf {
+ protoc {
+ artifact = 'com.google.protobuf:protoc:3.5.1-1'
+ }
+ plugins {
+ grpc {
+ artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
+ }
+ }
+ generateProtoTasks {
+ all()*.plugins {
+ grpc {}
+ }
+ ofSourceSet('main')
+ }
+}
+
+// Inform IDEs like IntelliJ IDEA, Eclipse or NetBeans about the generated code.
+sourceSets {
+ main {
+ java {
+ srcDir 'src'
+ srcDirs 'build/generated/source/proto/main/grpc'
+ srcDirs 'build/generated/source/proto/main/java'
+ }
+ }
+}
+
+// Provide convenience executables for trying out the examples.
+apply plugin: 'application'
+
+startScripts.enabled = false
+
+task tagContextExample(type: CreateStartScripts) {
+ mainClassName = 'io.opencensus.examples.tags.TagContextExample'
+ applicationName = 'TagContextExample'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task multiSpansTracing(type: CreateStartScripts) {
+ mainClassName = 'io.opencensus.examples.trace.MultiSpansTracing'
+ applicationName = 'MultiSpansTracing'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task multiSpansScopedTracing(type: CreateStartScripts) {
+ mainClassName = 'io.opencensus.examples.trace.MultiSpansScopedTracing'
+ applicationName = 'MultiSpansScopedTracing'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task multiSpansContextTracing(type: CreateStartScripts) {
+ mainClassName = 'io.opencensus.examples.trace.MultiSpansContextTracing'
+ applicationName = 'MultiSpansContextTracing'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task zPagesTester(type: CreateStartScripts) {
+ mainClassName = 'io.opencensus.examples.zpages.ZPagesTester'
+ applicationName = 'ZPagesTester'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task quickStart(type: CreateStartScripts) {
+ mainClassName = 'io.opencensus.examples.helloworld.QuickStart'
+ applicationName = 'QuickStart'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task helloWorldServer(type: CreateStartScripts) {
+ mainClassName = 'io.opencensus.examples.grpc.helloworld.HelloWorldServer'
+ applicationName = 'HelloWorldServer'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = jar.outputs.files + project.configurations.runtime
+}
+
+task helloWorldClient(type: CreateStartScripts) {
+ mainClassName = 'io.opencensus.examples.grpc.helloworld.HelloWorldClient'
+ applicationName = 'HelloWorldClient'
+ outputDir = new File(project.buildDir, 'tmp')
+ classpath = jar.outputs.files + project.configurations.runtime
+}
+
+applicationDistribution.into('bin') {
+ from(multiSpansTracing)
+ from(multiSpansScopedTracing)
+ from(multiSpansContextTracing)
+ from(tagContextExample)
+ from(zPagesTester)
+ from(quickStart)
+ from(helloWorldServer)
+ from(helloWorldClient)
+ fileMode = 0755
+}
diff --git a/examples/gradle/wrapper/gradle-wrapper.jar b/examples/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..758de960
--- /dev/null
+++ b/examples/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..a95009c3
--- /dev/null
+++ b/examples/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/examples/gradlew b/examples/gradlew
new file mode 100755
index 00000000..cccdd3d5
--- /dev/null
+++ b/examples/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/examples/gradlew.bat b/examples/gradlew.bat
new file mode 100644
index 00000000..e95643d6
--- /dev/null
+++ b/examples/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/opencensus_workspace.bzl b/examples/opencensus_workspace.bzl
new file mode 100644
index 00000000..ce382cd2
--- /dev/null
+++ b/examples/opencensus_workspace.bzl
@@ -0,0 +1,1680 @@
+# The following dependencies were calculated from:
+#
+# generate_workspace --artifact=com.google.guava:guava-jdk5:23.0 --artifact=com.google.guava:guava:23.0 --artifact=io.grpc:grpc-all:1.9.0 --artifact=io.opencensus:opencensus-api:0.16.1 --artifact=io.opencensus:opencensus-contrib-grpc-metrics:0.16.1 --artifact=io.opencensus:opencensus-contrib-zpages:0.16.1 --artifact=io.opencensus:opencensus-exporter-stats-prometheus:0.16.1 --artifact=io.opencensus:opencensus-exporter-stats-stackdriver:0.16.1 --artifact=io.opencensus:opencensus-exporter-trace-logging:0.16.1 --artifact=io.opencensus:opencensus-exporter-trace-stackdriver:0.16.1 --artifact=io.opencensus:opencensus-impl:0.16.1 --artifact=io.prometheus:simpleclient_httpserver:0.3.0 --repositories=http://repo.maven.apache.org/maven2
+
+
+def opencensus_maven_jars():
+ # io.opencensus:opencensus-api:jar:0.10.0 wanted version 3.0.1
+ # io.grpc:grpc-core:jar:1.9.0 wanted version 3.0.0
+ # com.google.guava:guava:bundle:23.0
+ # com.google.instrumentation:instrumentation-api:jar:0.4.3 wanted version 3.0.0
+ # io.opencensus:opencensus-contrib-grpc-metrics:jar:0.10.0 wanted version 3.0.1
+ native.maven_jar(
+ name = "com_google_code_findbugs_jsr305",
+ artifact = "com.google.code.findbugs:jsr305:2.0.2",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "516c03b21d50a644d538de0f0369c620989cd8f0",
+ )
+
+
+ # io.grpc:grpc-protobuf:jar:1.9.0
+ native.maven_jar(
+ name = "io_grpc_grpc_protobuf_lite",
+ artifact = "io.grpc:grpc-protobuf-lite:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "9dc9c6531ae0b304581adff0e9b7cff21a4073ac",
+ )
+
+
+ native.maven_jar(
+ name = "io_opencensus_opencensus_exporter_stats_prometheus",
+ artifact = "io.opencensus:opencensus-exporter-stats-prometheus:0.16.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "c1e9fc26da3060dde5a5948fd065c1b28cd10f39",
+ )
+
+
+ # com.google.api:gax-grpc:jar:1.30.0 got requested version
+ # com.google.api:gax:jar:1.30.0
+ native.maven_jar(
+ name = "com_google_auth_google_auth_library_oauth2_http",
+ artifact = "com.google.auth:google-auth-library-oauth2-http:0.10.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "c079a62086121973a23d90f54e2b8c13050fa39d",
+ )
+
+
+ # io.netty:netty-handler-proxy:jar:4.1.17.Final got requested version
+ # io.netty:netty-codec:jar:4.1.17.Final
+ # io.netty:netty-handler:jar:4.1.17.Final got requested version
+ native.maven_jar(
+ name = "io_netty_netty_transport",
+ artifact = "io.netty:netty-transport:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "9585776b0a8153182412b5d5366061ff486914c1",
+ )
+
+
+ # io.grpc:grpc-netty:jar:1.9.0
+ native.maven_jar(
+ name = "io_netty_netty_handler_proxy",
+ artifact = "io.netty:netty-handler-proxy:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "9330ee60c4e48ca60aac89b7bc5ec2567e84f28e",
+ )
+
+
+ # io.grpc:grpc-all:jar:1.9.0
+ native.maven_jar(
+ name = "io_grpc_grpc_protobuf_nano",
+ artifact = "io.grpc:grpc-protobuf-nano:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "561b03d3fd5178117a51f9f7ef9d9e5442ed2348",
+ )
+
+
+ # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1
+ native.maven_jar(
+ name = "com_google_cloud_google_cloud_trace",
+ artifact = "com.google.cloud:google-cloud-trace:0.58.0-beta",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "ea715c51340a32106ffdf32375a5dad9dbdf160e",
+ )
+
+
+ # org.apache.httpcomponents:httpclient:jar:4.5.3
+ native.maven_jar(
+ name = "commons_codec_commons_codec",
+ artifact = "commons-codec:commons-codec:1.9",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "9ce04e34240f674bc72680f8b843b1457383161a",
+ )
+
+
+ # io.opencensus:opencensus-impl:jar:0.16.1
+ native.maven_jar(
+ name = "io_opencensus_opencensus_impl_core",
+ artifact = "io.opencensus:opencensus-impl-core:0.16.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "a87fc041f66b8c923e2a1de6b7c1582b7990fde8",
+ )
+
+
+ # io.prometheus:simpleclient_httpserver:bundle:0.4.0
+ native.maven_jar(
+ name = "io_prometheus_simpleclient_common",
+ artifact = "io.prometheus:simpleclient_common:0.3.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "c9656d515d3a7647407f2c221d56be13177b82a0",
+ )
+
+
+ # com.google.api:gax-grpc:jar:1.30.0 got requested version
+ # com.google.api:gax:jar:1.30.0
+ native.maven_jar(
+ name = "org_threeten_threetenbp",
+ artifact = "org.threeten:threetenbp:1.3.3",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "3ea31c96676ff12ab56be0b1af6fff61d1a4f1f2",
+ )
+
+
+ # io.grpc:grpc-core:jar:1.9.0 wanted version 2.1.2
+ # io.opencensus:opencensus-contrib-grpc-metrics:jar:0.10.0 wanted version 2.1.2
+ # com.google.guava:guava:bundle:23.0
+ # io.opencensus:opencensus-api:jar:0.10.0 wanted version 2.1.2
+ native.maven_jar(
+ name = "com_google_errorprone_error_prone_annotations",
+ artifact = "com.google.errorprone:error_prone_annotations:2.0.18",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "5f65affce1684999e2f4024983835efc3504012e",
+ )
+
+
+ # io.netty:netty-transport:jar:4.1.17.Final
+ native.maven_jar(
+ name = "io_netty_netty_resolver",
+ artifact = "io.netty:netty-resolver:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "8f386c80821e200f542da282ae1d3cde5cad8368",
+ )
+
+
+ # com.squareup.okhttp:okhttp:jar:2.5.0
+ # io.grpc:grpc-okhttp:jar:1.9.0 wanted version 1.13.0
+ native.maven_jar(
+ name = "com_squareup_okio_okio",
+ artifact = "com.squareup.okio:okio:1.6.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "98476622f10715998eacf9240d6b479f12c66143",
+ )
+
+
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 3.6.0
+ # io.grpc:grpc-protobuf:jar:1.9.0
+ # com.google.cloud:google-cloud-core:jar:1.40.0 wanted version 3.6.0
+ native.maven_jar(
+ name = "com_google_protobuf_protobuf_java_util",
+ artifact = "com.google.protobuf:protobuf-java-util:3.5.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "6e40a6a3f52455bd633aa2a0dba1a416e62b4575",
+ )
+
+
+ # io.grpc:grpc-auth:jar:1.9.0
+ # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1 wanted version 0.10.0
+ # com.google.api:gax-grpc:jar:1.30.0 wanted version 0.10.0
+ # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1 wanted version 0.10.0
+ # com.google.auth:google-auth-library-oauth2-http:jar:0.9.0 got requested version
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 0.10.0
+ native.maven_jar(
+ name = "com_google_auth_google_auth_library_credentials",
+ artifact = "com.google.auth:google-auth-library-credentials:0.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "8e2b181feff6005c9cbc6f5c1c1e2d3ec9138d46",
+ )
+
+
+ # com.google.api.grpc:proto-google-cloud-trace-v2:jar:0.23.0 got requested version
+ # com.google.api:gax:jar:1.30.0 got requested version
+ # com.google.api.grpc:proto-google-cloud-trace-v1:jar:0.23.0 got requested version
+ # com.google.api.grpc:proto-google-iam-v1:jar:0.12.0 wanted version 1.5.0
+ # com.google.api.grpc:proto-google-cloud-monitoring-v3:jar:1.22.0 got requested version
+ # com.google.cloud:google-cloud-core:jar:1.40.0
+ # com.google.api:gax-grpc:jar:1.30.0 got requested version
+ native.maven_jar(
+ name = "com_google_api_api_common",
+ artifact = "com.google.api:api-common:1.7.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "ea59fb8b2450999345035dec8a6f472543391766",
+ )
+
+
+ # io.opencensus:opencensus-contrib-zpages:jar:0.16.1 got requested version
+ native.maven_jar(
+ name = "io_opencensus_opencensus_contrib_grpc_metrics",
+ artifact = "io.opencensus:opencensus-contrib-grpc-metrics:0.16.1",
+ sha1 = "f56b444e2766eaf597ee11c7501f0d6b9992395c",
+ )
+
+
+ # org.mockito:mockito-core:jar:1.9.5
+ native.maven_jar(
+ name = "org_objenesis_objenesis",
+ artifact = "org.objenesis:objenesis:1.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "9b473564e792c2bdf1449da1f0b1b5bff9805704",
+ )
+
+
+ # io.netty:netty-buffer:jar:4.1.17.Final
+ # io.netty:netty-resolver:jar:4.1.17.Final got requested version
+ native.maven_jar(
+ name = "io_netty_netty_common",
+ artifact = "io.netty:netty-common:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "581c8ee239e4dc0976c2405d155f475538325098",
+ )
+
+
+ # com.google.cloud:google-cloud-trace:jar:0.58.0-beta
+ native.maven_jar(
+ name = "com_google_api_grpc_proto_google_cloud_trace_v2",
+ artifact = "com.google.api.grpc:proto-google-cloud-trace-v2:0.23.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "4aa1bc7212d34791a02962092deafc43a7f4245e",
+ )
+
+
+ # com.google.cloud:google-cloud-trace:jar:0.58.0-beta got requested version
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0
+ # com.google.cloud:google-cloud-monitoring:jar:1.40.0 got requested version
+ native.maven_jar(
+ name = "io_grpc_grpc_netty_shaded",
+ artifact = "io.grpc:grpc-netty-shaded:1.13.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "ccdc4f2c2791d93164c574fbfb90d614aa0849ae",
+ )
+
+
+ # com.google.cloud:google-cloud-trace:jar:0.58.0-beta
+ native.maven_jar(
+ name = "com_google_api_grpc_proto_google_cloud_trace_v1",
+ artifact = "com.google.api.grpc:proto-google-cloud-trace-v1:0.23.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "848bb2c3b9d683dccc2a26d077015cdc71b7e343",
+ )
+
+
+ # io.grpc:grpc-all:jar:1.9.0
+ native.maven_jar(
+ name = "io_grpc_grpc_okhttp",
+ artifact = "io.grpc:grpc-okhttp:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "4e7fbb9d3cd65848f42494de165b1c5839f69a8a",
+ )
+
+
+ # junit:junit:jar:4.12
+ native.maven_jar(
+ name = "org_hamcrest_hamcrest_core",
+ artifact = "org.hamcrest:hamcrest-core:1.3",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "42a25dc3219429f0e5d060061f71acb49bf010a0",
+ )
+
+
+ # io.netty:netty-codec-http2:jar:4.1.17.Final
+ native.maven_jar(
+ name = "io_netty_netty_handler",
+ artifact = "io.netty:netty-handler:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "18c40ffb61a1d1979eca024087070762fdc4664a",
+ )
+
+
+ # com.google.cloud:google-cloud-monitoring:jar:1.40.0
+ native.maven_jar(
+ name = "com_google_api_grpc_proto_google_cloud_monitoring_v3",
+ artifact = "com.google.api.grpc:proto-google-cloud-monitoring-v3:1.22.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "5b8746703e9d8f2937d4925a70b030cfc5bf00f6",
+ )
+
+
+ # com.google.auth:google-auth-library-oauth2-http:jar:0.9.0 wanted version 1.19.0
+ # com.google.cloud:google-cloud-core:jar:1.40.0
+ native.maven_jar(
+ name = "com_google_http_client_google_http_client",
+ artifact = "com.google.http-client:google-http-client:1.24.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "396eac8d3fb1332675f82b208f48a469d64f3b4a",
+ )
+
+
+ native.maven_jar(
+ name = "io_prometheus_simpleclient_httpserver",
+ artifact = "io.prometheus:simpleclient_httpserver:0.3.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "a2c1aeecac28f5bfa9a92a67b071d246ac00bbec",
+ )
+
+
+ # io.grpc:grpc-core:jar:1.9.0
+ native.maven_jar(
+ name = "com_google_instrumentation_instrumentation_api",
+ artifact = "com.google.instrumentation:instrumentation-api:0.4.3",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "41614af3429573dc02645d541638929d877945a2",
+ )
+
+
+ # com.google.auth:google-auth-library-oauth2-http:jar:0.9.0
+ native.maven_jar(
+ name = "com_google_http_client_google_http_client_jackson2",
+ artifact = "com.google.http-client:google-http-client-jackson2:1.19.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "81dbf9795d387d5e80e55346582d5f2fb81a42eb",
+ )
+
+
+ native.maven_jar(
+ name = "io_opencensus_opencensus_exporter_trace_logging",
+ artifact = "io.opencensus:opencensus-exporter-trace-logging:0.16.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "a3ca83ff7075c58e564aa029c35ccd8224616879",
+ )
+
+
+ # com.google.api:gax-grpc:jar:1.30.0 wanted version 1.13.1
+ # io.grpc:grpc-all:jar:1.9.0
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 1.13.1
+ # com.google.cloud:google-cloud-monitoring:jar:1.40.0 wanted version 1.13.1
+ # com.google.cloud:google-cloud-trace:jar:0.58.0-beta wanted version 1.13.1
+ native.maven_jar(
+ name = "io_grpc_grpc_auth",
+ artifact = "io.grpc:grpc-auth:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "d2eadc6d28ebee8ec0cef74f882255e4069972ad",
+ )
+
+
+ # com.google.cloud:google-cloud-core:jar:1.40.0
+ # com.google.api:gax-grpc:jar:1.30.0 got requested version
+ native.maven_jar(
+ name = "com_google_api_gax",
+ artifact = "com.google.api:gax:1.30.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "58fa2feb11b092be0a6ebe705a28736f12374230",
+ )
+
+
+ native.maven_jar(
+ name = "io_opencensus_opencensus_exporter_trace_stackdriver",
+ artifact = "io.opencensus:opencensus-exporter-trace-stackdriver:0.16.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "6ea1a99a5cc580f472fbddf34152b3dcd6929e88",
+ )
+
+
+ # com.google.guava:guava:bundle:23.0
+ native.maven_jar(
+ name = "com_google_j2objc_j2objc_annotations",
+ artifact = "com.google.j2objc:j2objc-annotations:1.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "ed28ded51a8b1c6b112568def5f4b455e6809019",
+ )
+
+
+ # io.grpc:grpc-auth:jar:1.9.0
+ # io.grpc:grpc-protobuf:jar:1.9.0 got requested version
+ # io.grpc:grpc-okhttp:jar:1.9.0 got requested version
+ # io.grpc:grpc-stub:jar:1.9.0 got requested version
+ # io.grpc:grpc-protobuf-lite:jar:1.9.0 got requested version
+ # io.grpc:grpc-all:jar:1.9.0 got requested version
+ # io.grpc:grpc-protobuf-nano:jar:1.9.0 got requested version
+ # io.grpc:grpc-testing:jar:1.9.0 got requested version
+ # io.grpc:grpc-netty:jar:1.9.0 got requested version
+ # io.grpc:grpc-netty-shaded:jar:1.13.1 wanted version 1.13.1
+ native.maven_jar(
+ name = "io_grpc_grpc_core",
+ artifact = "io.grpc:grpc-core:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "cf76ab13d35e8bd5d0ffad6d82bb1ef1770f050c",
+ )
+
+
+ # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1
+ # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1 got requested version
+ native.maven_jar(
+ name = "io_opencensus_opencensus_contrib_monitored_resource_util",
+ artifact = "io.opencensus:opencensus-contrib-monitored-resource-util:0.16.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "9edb4161978ac89f99a69544bfdc71b018a2509d",
+ )
+
+
+ # com.google.cloud:google-cloud-core:jar:1.40.0
+ native.maven_jar(
+ name = "joda_time_joda_time",
+ artifact = "joda-time:joda-time:2.9.2",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "36d6e77a419cb455e6fd5909f6f96b168e21e9d0",
+ )
+
+
+ # io.grpc:grpc-testing:jar:1.9.0
+ native.maven_jar(
+ name = "org_mockito_mockito_core",
+ artifact = "org.mockito:mockito-core:1.9.5",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "c3264abeea62c4d2f367e21484fbb40c7e256393",
+ )
+
+
+ # org.apache.httpcomponents:httpclient:jar:4.5.3
+ native.maven_jar(
+ name = "org_apache_httpcomponents_httpcore",
+ artifact = "org.apache.httpcomponents:httpcore:4.4.6",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "e3fd8ced1f52c7574af952e2e6da0df8df08eb82",
+ )
+
+
+ # io.opencensus:opencensus-impl:jar:0.16.1
+ native.maven_jar(
+ name = "com_lmax_disruptor",
+ artifact = "com.lmax:disruptor:3.4.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "72fabfe8a183f53bf61e0303921b7a89d2e8daed",
+ )
+
+
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 3.6.0
+ # com.google.api.grpc:proto-google-cloud-trace-v1:jar:0.23.0 wanted version 3.6.0
+ # com.google.api.grpc:proto-google-cloud-monitoring-v3:jar:1.22.0 wanted version 3.6.0
+ # io.grpc:grpc-protobuf:jar:1.9.0
+ # com.google.api.grpc:proto-google-iam-v1:jar:0.12.0 got requested version
+ # com.google.protobuf:protobuf-java-util:bundle:3.5.1 got requested version
+ # com.google.api.grpc:proto-google-cloud-trace-v2:jar:0.23.0 wanted version 3.6.0
+ native.maven_jar(
+ name = "com_google_protobuf_protobuf_java",
+ artifact = "com.google.protobuf:protobuf-java:3.5.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "8c3492f7662fa1cbf8ca76a0f5eb1146f7725acd",
+ )
+
+
+ # io.grpc:grpc-okhttp:jar:1.9.0
+ native.maven_jar(
+ name = "com_squareup_okhttp_okhttp",
+ artifact = "com.squareup.okhttp:okhttp:2.5.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "4de2b4ed3445c37ec1720a7d214712e845a24636",
+ )
+
+
+ # io.grpc:grpc-testing:jar:1.9.0 got requested version
+ # com.google.api:gax-grpc:jar:1.30.0 wanted version 1.13.1
+ # io.grpc:grpc-all:jar:1.9.0
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 1.13.1
+ # com.google.cloud:google-cloud-monitoring:jar:1.40.0 wanted version 1.13.1
+ # com.google.cloud:google-cloud-trace:jar:0.58.0-beta wanted version 1.13.1
+ native.maven_jar(
+ name = "io_grpc_grpc_stub",
+ artifact = "io.grpc:grpc-stub:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "20e310f888860a27dfa509a69eebb236417ee93f",
+ )
+
+
+ native.maven_jar(
+ name = "io_opencensus_opencensus_impl",
+ artifact = "io.opencensus:opencensus-impl:0.16.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "f9b06bf8422ba3700346173524087d005725432e",
+ )
+
+
+ # com.google.api:gax-grpc:jar:1.30.0 wanted version 1.13.1
+ # io.grpc:grpc-all:jar:1.9.0
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 1.13.1
+ native.maven_jar(
+ name = "io_grpc_grpc_protobuf",
+ artifact = "io.grpc:grpc-protobuf:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "94ca247577e4cf1a38d5ac9d536ac1d426a1ccc5",
+ )
+
+
+ # io.netty:netty-handler-proxy:jar:4.1.17.Final
+ native.maven_jar(
+ name = "io_netty_netty_codec_socks",
+ artifact = "io.netty:netty-codec-socks:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "a159bf1f3d5019e0d561c92fbbec8400967471fa",
+ )
+
+
+ # io.netty:netty-codec-http:jar:4.1.17.Final
+ # io.netty:netty-codec-socks:jar:4.1.17.Final got requested version
+ # io.netty:netty-handler:jar:4.1.17.Final got requested version
+ native.maven_jar(
+ name = "io_netty_netty_codec",
+ artifact = "io.netty:netty-codec:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "1d00f56dc9e55203a4bde5aae3d0828fdeb818e7",
+ )
+
+
+ # io.netty:netty-transport:jar:4.1.17.Final
+ # io.netty:netty-handler:jar:4.1.17.Final got requested version
+ native.maven_jar(
+ name = "io_netty_netty_buffer",
+ artifact = "io.netty:netty-buffer:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "fdd68fb3defd7059a7392b9395ee941ef9bacc25",
+ )
+
+
+ # com.google.cloud:google-cloud-trace:jar:0.58.0-beta got requested version
+ # com.google.cloud:google-cloud-monitoring:jar:1.40.0
+ native.maven_jar(
+ name = "com_google_cloud_google_cloud_core_grpc",
+ artifact = "com.google.cloud:google-cloud-core-grpc:1.40.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "f1f7a81915728eb53b9d3832f3ccec53ea181664",
+ )
+
+
+ # io.grpc:grpc-all:jar:1.9.0
+ native.maven_jar(
+ name = "io_grpc_grpc_netty",
+ artifact = "io.grpc:grpc-netty:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "8157384d87497dc18604a5ba3760763fe643f16e",
+ )
+
+
+ # io.grpc:grpc-all:jar:1.9.0
+ native.maven_jar(
+ name = "io_grpc_grpc_testing",
+ artifact = "io.grpc:grpc-testing:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "3d20675f0e64825f565a7d21456e7dbdd5886c6b",
+ )
+
+
+ # io.opencensus:opencensus-impl:jar:0.16.1 got requested version
+ # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1 got requested version
+ # io.opencensus:opencensus-exporter-trace-logging:jar:0.16.1 got requested version
+ # io.opencensus:opencensus-contrib-grpc-metrics:jar:0.10.0 wanted version 0.10.0
+ # io.opencensus:opencensus-exporter-stats-prometheus:jar:0.16.1 got requested version
+ # io.opencensus:opencensus-contrib-zpages:jar:0.16.1 got requested version
+ # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1 got requested version
+ # io.opencensus:opencensus-impl-core:jar:0.16.1 got requested version
+ native.maven_jar(
+ name = "io_opencensus_opencensus_api",
+ artifact = "io.opencensus:opencensus-api:0.16.1",
+ sha1 = "ec5d81a80d9c010c50368ad9045d512828d0d62d",
+ )
+
+
+ # io.grpc:grpc-testing:jar:1.9.0
+ native.maven_jar(
+ name = "junit_junit",
+ artifact = "junit:junit:4.12",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "2973d150c0dc1fefe998f834810d68f278ea58ec",
+ )
+
+
+ # io.prometheus:simpleclient_httpserver:bundle:0.4.0 wanted version 0.3.0
+ # io.prometheus:simpleclient_common:bundle:0.4.0 wanted version 0.3.0
+ # io.opencensus:opencensus-exporter-stats-prometheus:jar:0.16.1
+ native.maven_jar(
+ name = "io_prometheus_simpleclient",
+ artifact = "io.prometheus:simpleclient:0.4.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "99c293bbf9461587b2179273b6fdc349582a1021",
+ )
+
+
+ # com.google.guava:guava:bundle:23.0
+ native.maven_jar(
+ name = "org_codehaus_mojo_animal_sniffer_annotations",
+ artifact = "org.codehaus.mojo:animal-sniffer-annotations:1.14",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "775b7e22fb10026eed3f86e8dc556dfafe35f2d5",
+ )
+
+
+ native.maven_jar(
+ name = "io_opencensus_opencensus_exporter_stats_stackdriver",
+ artifact = "io.opencensus:opencensus-exporter-stats-stackdriver:0.16.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "e4e7152e53c7683e92a1ddae15a2e13eeaa7714e",
+ )
+
+
+ # io.netty:netty-handler-proxy:jar:4.1.17.Final got requested version
+ # io.netty:netty-codec-http2:jar:4.1.17.Final
+ native.maven_jar(
+ name = "io_netty_netty_codec_http",
+ artifact = "io.netty:netty-codec-http:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "251d7edcb897122b9b23f24ff793cd0739056b9e",
+ )
+
+
+ # org.apache.httpcomponents:httpclient:jar:4.5.3
+ native.maven_jar(
+ name = "commons_logging_commons_logging",
+ artifact = "commons-logging:commons-logging:1.2",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "4bfc12adfe4842bf07b657f0369c4cb522955686",
+ )
+
+
+ # io.grpc:grpc-netty:jar:1.9.0
+ native.maven_jar(
+ name = "io_netty_netty_codec_http2",
+ artifact = "io.netty:netty-codec-http2:4.1.17.Final",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "f9844005869c6d9049f4b677228a89fee4c6eab3",
+ )
+
+
+ # com.google.protobuf:protobuf-java-util:bundle:3.5.1
+ native.maven_jar(
+ name = "com_google_code_gson_gson",
+ artifact = "com.google.code.gson:gson:2.7",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "751f548c85fa49f330cecbb1875893f971b33c4e",
+ )
+
+
+ # io.grpc:grpc-protobuf-nano:jar:1.9.0
+ native.maven_jar(
+ name = "com_google_protobuf_nano_protobuf_javanano",
+ artifact = "com.google.protobuf.nano:protobuf-javanano:3.0.0-alpha-5",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "357e60f95cebb87c72151e49ba1f570d899734f8",
+ )
+
+
+ # com.google.http-client:google-http-client:jar:1.24.1
+ native.maven_jar(
+ name = "org_apache_httpcomponents_httpclient",
+ artifact = "org.apache.httpcomponents:httpclient:4.5.3",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "d1577ae15f01ef5438c5afc62162457c00a34713",
+ )
+
+
+ # com.google.cloud:google-cloud-core:jar:1.40.0
+ native.maven_jar(
+ name = "com_google_api_grpc_proto_google_iam_v1",
+ artifact = "com.google.api.grpc:proto-google-iam-v1:0.12.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "ea312c0250a5d0a7cdd1b20bc2c3259938b79855",
+ )
+
+
+ # io.opencensus:opencensus-api:jar:0.10.0 wanted version 1.8.0
+ # io.grpc:grpc-all:jar:1.9.0 got requested version
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 wanted version 1.13.1
+ # io.grpc:grpc-core:jar:1.9.0
+ native.maven_jar(
+ name = "io_grpc_grpc_context",
+ artifact = "io.grpc:grpc-context:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "28b0836f48c9705abf73829bbc536dba29a1329a",
+ )
+
+
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0
+ native.maven_jar(
+ name = "com_google_api_gax_grpc",
+ artifact = "com.google.api:gax-grpc:1.30.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "ada82a4a0c020807e1c1a674b18658374264e401",
+ )
+
+
+ # com.google.api.grpc:proto-google-cloud-monitoring-v3:jar:1.22.0 wanted version 1.12.0
+ # com.google.api.grpc:proto-google-iam-v1:jar:0.12.0 wanted version 1.11.0
+ # com.google.api:gax-grpc:jar:1.30.0 wanted version 1.12.0
+ # com.google.api.grpc:proto-google-cloud-trace-v1:jar:0.23.0 wanted version 1.12.0
+ # io.grpc:grpc-protobuf:jar:1.9.0
+ # com.google.api.grpc:proto-google-cloud-trace-v2:jar:0.23.0 wanted version 1.12.0
+ # com.google.cloud:google-cloud-core:jar:1.40.0 wanted version 1.12.0
+ native.maven_jar(
+ name = "com_google_api_grpc_proto_google_common_protos",
+ artifact = "com.google.api.grpc:proto-google-common-protos:1.0.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "86f070507e28b930e50d218ee5b6788ef0dd05e6",
+ )
+
+
+ native.maven_jar(
+ name = "io_opencensus_opencensus_contrib_zpages",
+ artifact = "io.opencensus:opencensus-contrib-zpages:0.16.1",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "5fe09e41a9435281eb4547bc57ae34b9fd6bbf21",
+ )
+
+
+ # io.opencensus:opencensus-exporter-trace-stackdriver:jar:0.16.1 wanted version 20.0
+ # io.opencensus:opencensus-exporter-stats-prometheus:jar:0.16.1 wanted version 20.0
+ # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1 wanted version 20.0
+ # io.grpc:grpc-protobuf-lite:jar:1.9.0 wanted version 19.0
+ # com.google.instrumentation:instrumentation-api:jar:0.4.3 wanted version 19.0
+ # io.grpc:grpc-protobuf:jar:1.9.0 wanted version 19.0
+ # io.opencensus:opencensus-contrib-zpages:jar:0.16.1 wanted version 20.0
+ # io.opencensus:opencensus-impl-core:jar:0.16.1 wanted version 20.0
+ # io.opencensus:opencensus-exporter-trace-logging:jar:0.16.1 wanted version 20.0
+ # io.grpc:grpc-protobuf-nano:jar:1.9.0 wanted version 19.0
+ # io.grpc:grpc-core:jar:1.9.0 wanted version 19.0
+ # com.google.protobuf:protobuf-java-util:bundle:3.5.1 wanted version 19.0
+ # io.opencensus:opencensus-api:jar:0.10.0 wanted version 19.0
+ native.maven_jar(
+ name = "com_google_guava_guava",
+ artifact = "com.google.guava:guava:23.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "c947004bb13d18182be60077ade044099e4f26f1",
+ )
+
+
+ native.maven_jar(
+ name = "io_grpc_grpc_all",
+ artifact = "io.grpc:grpc-all:1.9.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "442dfac27fd072e15b7134ab02c2b38136036090",
+ )
+
+
+ # com.google.cloud:google-cloud-core-grpc:jar:1.40.0 got requested version
+ # com.google.cloud:google-cloud-trace:jar:0.58.0-beta got requested version
+ # com.google.cloud:google-cloud-monitoring:jar:1.40.0
+ native.maven_jar(
+ name = "com_google_cloud_google_cloud_core",
+ artifact = "com.google.cloud:google-cloud-core:1.40.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "4985701f989030e262cf8f4e38cc954115f5b082",
+ )
+
+
+ # io.opencensus:opencensus-exporter-stats-stackdriver:jar:0.16.1
+ native.maven_jar(
+ name = "com_google_cloud_google_cloud_monitoring",
+ artifact = "com.google.cloud:google-cloud-monitoring:1.40.0",
+ repository = "http://repo.maven.apache.org/maven2/",
+ sha1 = "f03d20d67a5f3b0cd0685225a6ea5339d208aa53",
+ )
+
+
+
+
+def opencensus_java_libraries():
+ native.java_library(
+ name = "com_google_code_findbugs_jsr305",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_code_findbugs_jsr305//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_protobuf_lite",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_protobuf_lite//jar"],
+ runtime_deps = [
+ ":com_google_guava_guava",
+ ":io_grpc_grpc_core",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_exporter_stats_prometheus",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_exporter_stats_prometheus//jar"],
+ runtime_deps = [
+ ":com_google_guava_guava",
+ ":io_opencensus_opencensus_api",
+ ":io_prometheus_simpleclient",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_auth_google_auth_library_oauth2_http",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_auth_google_auth_library_oauth2_http//jar"],
+ runtime_deps = [
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_http_client_google_http_client",
+ ":com_google_http_client_google_http_client_jackson2",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_transport",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_transport//jar"],
+ runtime_deps = [
+ ":io_netty_netty_buffer",
+ ":io_netty_netty_common",
+ ":io_netty_netty_resolver",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_handler_proxy",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_handler_proxy//jar"],
+ runtime_deps = [
+ ":io_netty_netty_codec",
+ ":io_netty_netty_codec_http",
+ ":io_netty_netty_codec_socks",
+ ":io_netty_netty_transport",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_protobuf_nano",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_protobuf_nano//jar"],
+ runtime_deps = [
+ ":com_google_guava_guava",
+ ":com_google_protobuf_nano_protobuf_javanano",
+ ":io_grpc_grpc_core",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_cloud_google_cloud_trace",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_cloud_google_cloud_trace//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_grpc_proto_google_cloud_trace_v1",
+ ":com_google_api_grpc_proto_google_cloud_trace_v2",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_cloud_google_cloud_core",
+ ":com_google_cloud_google_cloud_core_grpc",
+ ":com_google_protobuf_protobuf_java",
+ ":io_grpc_grpc_auth",
+ ":io_grpc_grpc_netty_shaded",
+ ":io_grpc_grpc_stub",
+ ],
+ )
+
+
+ native.java_library(
+ name = "commons_codec_commons_codec",
+ visibility = ["//visibility:public"],
+ exports = ["@commons_codec_commons_codec//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_impl_core",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_impl_core//jar"],
+ runtime_deps = [
+ ":com_google_guava_guava",
+ ":io_opencensus_opencensus_api",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_prometheus_simpleclient_common",
+ visibility = ["//visibility:public"],
+ exports = ["@io_prometheus_simpleclient_common//jar"],
+ runtime_deps = [
+ ":io_prometheus_simpleclient",
+ ],
+ )
+
+
+ native.java_library(
+ name = "org_threeten_threetenbp",
+ visibility = ["//visibility:public"],
+ exports = ["@org_threeten_threetenbp//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_google_errorprone_error_prone_annotations",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_errorprone_error_prone_annotations//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_resolver",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_resolver//jar"],
+ runtime_deps = [
+ ":io_netty_netty_common",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_squareup_okio_okio",
+ visibility = ["//visibility:public"],
+ exports = ["@com_squareup_okio_okio//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_google_protobuf_protobuf_java_util",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_protobuf_protobuf_java_util//jar"],
+ runtime_deps = [
+ ":com_google_code_gson_gson",
+ ":com_google_guava_guava",
+ ":com_google_protobuf_protobuf_java",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_auth_google_auth_library_credentials",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_auth_google_auth_library_credentials//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_google_api_api_common",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_api_api_common//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_contrib_grpc_metrics",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_contrib_grpc_metrics//jar"],
+ runtime_deps = [
+ ":com_google_code_findbugs_jsr305",
+ ":com_google_errorprone_error_prone_annotations",
+ ":io_opencensus_opencensus_api",
+ ],
+ )
+
+
+ native.java_library(
+ name = "org_objenesis_objenesis",
+ visibility = ["//visibility:public"],
+ exports = ["@org_objenesis_objenesis//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_common",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_common//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_google_api_grpc_proto_google_cloud_trace_v2",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_api_grpc_proto_google_cloud_trace_v2//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_protobuf_protobuf_java",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_netty_shaded",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_netty_shaded//jar"],
+ runtime_deps = [
+ ":io_grpc_grpc_core",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_api_grpc_proto_google_cloud_trace_v1",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_api_grpc_proto_google_cloud_trace_v1//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_protobuf_protobuf_java",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_okhttp",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_okhttp//jar"],
+ runtime_deps = [
+ ":com_squareup_okhttp_okhttp",
+ ":com_squareup_okio_okio",
+ ":io_grpc_grpc_core",
+ ],
+ )
+
+
+ native.java_library(
+ name = "org_hamcrest_hamcrest_core",
+ visibility = ["//visibility:public"],
+ exports = ["@org_hamcrest_hamcrest_core//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_handler",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_handler//jar"],
+ runtime_deps = [
+ ":io_netty_netty_buffer",
+ ":io_netty_netty_codec",
+ ":io_netty_netty_transport",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_api_grpc_proto_google_cloud_monitoring_v3",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_api_grpc_proto_google_cloud_monitoring_v3//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_protobuf_protobuf_java",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_http_client_google_http_client",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_http_client_google_http_client//jar"],
+ runtime_deps = [
+ ":commons_codec_commons_codec",
+ ":commons_logging_commons_logging",
+ ":org_apache_httpcomponents_httpclient",
+ ":org_apache_httpcomponents_httpcore",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_prometheus_simpleclient_httpserver",
+ visibility = ["//visibility:public"],
+ exports = ["@io_prometheus_simpleclient_httpserver//jar"],
+ runtime_deps = [
+ ":io_prometheus_simpleclient",
+ ":io_prometheus_simpleclient_common",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_instrumentation_instrumentation_api",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_instrumentation_instrumentation_api//jar"],
+ runtime_deps = [
+ ":com_google_code_findbugs_jsr305",
+ ":com_google_guava_guava",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_http_client_google_http_client_jackson2",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_http_client_google_http_client_jackson2//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_exporter_trace_logging",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_exporter_trace_logging//jar"],
+ runtime_deps = [
+ ":com_google_guava_guava",
+ ":io_opencensus_opencensus_api",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_auth",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_auth//jar"],
+ runtime_deps = [
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_code_findbugs_jsr305",
+ ":com_google_errorprone_error_prone_annotations",
+ ":com_google_guava_guava",
+ ":com_google_instrumentation_instrumentation_api",
+ ":io_grpc_grpc_context",
+ ":io_grpc_grpc_core",
+ ":io_opencensus_opencensus_api",
+ ":io_opencensus_opencensus_contrib_grpc_metrics",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_api_gax",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_api_gax//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_auth_google_auth_library_oauth2_http",
+ ":com_google_http_client_google_http_client",
+ ":com_google_http_client_google_http_client_jackson2",
+ ":org_threeten_threetenbp",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_exporter_trace_stackdriver",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_exporter_trace_stackdriver//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_grpc_proto_google_cloud_trace_v1",
+ ":com_google_api_grpc_proto_google_cloud_trace_v2",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_cloud_google_cloud_core",
+ ":com_google_cloud_google_cloud_core_grpc",
+ ":com_google_cloud_google_cloud_trace",
+ ":com_google_guava_guava",
+ ":com_google_protobuf_protobuf_java",
+ ":io_grpc_grpc_auth",
+ ":io_grpc_grpc_netty_shaded",
+ ":io_grpc_grpc_stub",
+ ":io_opencensus_opencensus_api",
+ ":io_opencensus_opencensus_contrib_monitored_resource_util",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_j2objc_j2objc_annotations",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_j2objc_j2objc_annotations//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_core",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_core//jar"],
+ runtime_deps = [
+ ":com_google_code_findbugs_jsr305",
+ ":com_google_errorprone_error_prone_annotations",
+ ":com_google_guava_guava",
+ ":com_google_instrumentation_instrumentation_api",
+ ":io_grpc_grpc_context",
+ ":io_opencensus_opencensus_api",
+ ":io_opencensus_opencensus_contrib_grpc_metrics",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_contrib_monitored_resource_util",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_contrib_monitored_resource_util//jar"],
+ )
+
+
+ native.java_library(
+ name = "joda_time_joda_time",
+ visibility = ["//visibility:public"],
+ exports = ["@joda_time_joda_time//jar"],
+ )
+
+
+ native.java_library(
+ name = "org_mockito_mockito_core",
+ visibility = ["//visibility:public"],
+ exports = ["@org_mockito_mockito_core//jar"],
+ runtime_deps = [
+ ":org_objenesis_objenesis",
+ ],
+ )
+
+
+ native.java_library(
+ name = "org_apache_httpcomponents_httpcore",
+ visibility = ["//visibility:public"],
+ exports = ["@org_apache_httpcomponents_httpcore//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_lmax_disruptor",
+ visibility = ["//visibility:public"],
+ exports = ["@com_lmax_disruptor//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_google_protobuf_protobuf_java",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_protobuf_protobuf_java//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_squareup_okhttp_okhttp",
+ visibility = ["//visibility:public"],
+ exports = ["@com_squareup_okhttp_okhttp//jar"],
+ runtime_deps = [
+ ":com_squareup_okio_okio",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_stub",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_stub//jar"],
+ runtime_deps = [
+ ":io_grpc_grpc_core",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_impl",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_impl//jar"],
+ runtime_deps = [
+ ":com_google_guava_guava",
+ ":com_lmax_disruptor",
+ ":io_opencensus_opencensus_api",
+ ":io_opencensus_opencensus_impl_core",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_protobuf",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_protobuf//jar"],
+ runtime_deps = [
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_code_gson_gson",
+ ":com_google_guava_guava",
+ ":com_google_protobuf_protobuf_java",
+ ":com_google_protobuf_protobuf_java_util",
+ ":io_grpc_grpc_core",
+ ":io_grpc_grpc_protobuf_lite",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_codec_socks",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_codec_socks//jar"],
+ runtime_deps = [
+ ":io_netty_netty_codec",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_codec",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_codec//jar"],
+ runtime_deps = [
+ ":io_netty_netty_buffer",
+ ":io_netty_netty_common",
+ ":io_netty_netty_resolver",
+ ":io_netty_netty_transport",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_buffer",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_buffer//jar"],
+ runtime_deps = [
+ ":io_netty_netty_common",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_cloud_google_cloud_core_grpc",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_cloud_google_cloud_core_grpc//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_gax",
+ ":com_google_api_gax_grpc",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_auth_google_auth_library_oauth2_http",
+ ":com_google_cloud_google_cloud_core",
+ ":com_google_protobuf_protobuf_java",
+ ":com_google_protobuf_protobuf_java_util",
+ ":io_grpc_grpc_auth",
+ ":io_grpc_grpc_context",
+ ":io_grpc_grpc_core",
+ ":io_grpc_grpc_netty_shaded",
+ ":io_grpc_grpc_protobuf",
+ ":io_grpc_grpc_stub",
+ ":org_threeten_threetenbp",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_netty",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_netty//jar"],
+ runtime_deps = [
+ ":io_grpc_grpc_core",
+ ":io_netty_netty_buffer",
+ ":io_netty_netty_codec",
+ ":io_netty_netty_codec_http",
+ ":io_netty_netty_codec_http2",
+ ":io_netty_netty_codec_socks",
+ ":io_netty_netty_common",
+ ":io_netty_netty_handler",
+ ":io_netty_netty_handler_proxy",
+ ":io_netty_netty_resolver",
+ ":io_netty_netty_transport",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_testing",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_testing//jar"],
+ runtime_deps = [
+ ":io_grpc_grpc_core",
+ ":io_grpc_grpc_stub",
+ ":junit_junit",
+ ":org_hamcrest_hamcrest_core",
+ ":org_mockito_mockito_core",
+ ":org_objenesis_objenesis",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_api",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_api//jar"],
+ runtime_deps = [
+ ":com_google_code_findbugs_jsr305",
+ ":com_google_errorprone_error_prone_annotations",
+ ":com_google_guava_guava",
+ ":io_grpc_grpc_context",
+ ],
+ )
+
+
+ native.java_library(
+ name = "junit_junit",
+ visibility = ["//visibility:public"],
+ exports = ["@junit_junit//jar"],
+ runtime_deps = [
+ ":org_hamcrest_hamcrest_core",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_prometheus_simpleclient",
+ visibility = ["//visibility:public"],
+ exports = ["@io_prometheus_simpleclient//jar"],
+ )
+
+
+ native.java_library(
+ name = "org_codehaus_mojo_animal_sniffer_annotations",
+ visibility = ["//visibility:public"],
+ exports = ["@org_codehaus_mojo_animal_sniffer_annotations//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_exporter_stats_stackdriver",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_exporter_stats_stackdriver//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_gax",
+ ":com_google_api_gax_grpc",
+ ":com_google_api_grpc_proto_google_cloud_monitoring_v3",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_api_grpc_proto_google_iam_v1",
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_auth_google_auth_library_oauth2_http",
+ ":com_google_cloud_google_cloud_core",
+ ":com_google_cloud_google_cloud_core_grpc",
+ ":com_google_cloud_google_cloud_monitoring",
+ ":com_google_guava_guava",
+ ":com_google_http_client_google_http_client",
+ ":com_google_http_client_google_http_client_jackson2",
+ ":com_google_protobuf_protobuf_java",
+ ":com_google_protobuf_protobuf_java_util",
+ ":commons_codec_commons_codec",
+ ":commons_logging_commons_logging",
+ ":io_grpc_grpc_auth",
+ ":io_grpc_grpc_context",
+ ":io_grpc_grpc_core",
+ ":io_grpc_grpc_netty_shaded",
+ ":io_grpc_grpc_protobuf",
+ ":io_grpc_grpc_stub",
+ ":io_opencensus_opencensus_api",
+ ":io_opencensus_opencensus_contrib_monitored_resource_util",
+ ":joda_time_joda_time",
+ ":org_apache_httpcomponents_httpclient",
+ ":org_apache_httpcomponents_httpcore",
+ ":org_threeten_threetenbp",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_codec_http",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_codec_http//jar"],
+ runtime_deps = [
+ ":io_netty_netty_buffer",
+ ":io_netty_netty_codec",
+ ":io_netty_netty_common",
+ ":io_netty_netty_resolver",
+ ":io_netty_netty_transport",
+ ],
+ )
+
+
+ native.java_library(
+ name = "commons_logging_commons_logging",
+ visibility = ["//visibility:public"],
+ exports = ["@commons_logging_commons_logging//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_netty_netty_codec_http2",
+ visibility = ["//visibility:public"],
+ exports = ["@io_netty_netty_codec_http2//jar"],
+ runtime_deps = [
+ ":io_netty_netty_buffer",
+ ":io_netty_netty_codec",
+ ":io_netty_netty_codec_http",
+ ":io_netty_netty_common",
+ ":io_netty_netty_handler",
+ ":io_netty_netty_resolver",
+ ":io_netty_netty_transport",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_code_gson_gson",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_code_gson_gson//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_google_protobuf_nano_protobuf_javanano",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_protobuf_nano_protobuf_javanano//jar"],
+ )
+
+
+ native.java_library(
+ name = "org_apache_httpcomponents_httpclient",
+ visibility = ["//visibility:public"],
+ exports = ["@org_apache_httpcomponents_httpclient//jar"],
+ runtime_deps = [
+ ":commons_codec_commons_codec",
+ ":commons_logging_commons_logging",
+ ":org_apache_httpcomponents_httpcore",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_api_grpc_proto_google_iam_v1",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_api_grpc_proto_google_iam_v1//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_protobuf_protobuf_java",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_context",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_context//jar"],
+ )
+
+
+ native.java_library(
+ name = "com_google_api_gax_grpc",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_api_gax_grpc//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_gax",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_auth_google_auth_library_oauth2_http",
+ ":io_grpc_grpc_auth",
+ ":io_grpc_grpc_protobuf",
+ ":io_grpc_grpc_stub",
+ ":org_threeten_threetenbp",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_api_grpc_proto_google_common_protos",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_api_grpc_proto_google_common_protos//jar"],
+ )
+
+
+ native.java_library(
+ name = "io_opencensus_opencensus_contrib_zpages",
+ visibility = ["//visibility:public"],
+ exports = ["@io_opencensus_opencensus_contrib_zpages//jar"],
+ runtime_deps = [
+ ":com_google_guava_guava",
+ ":io_opencensus_opencensus_api",
+ ":io_opencensus_opencensus_contrib_grpc_metrics",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_guava_guava",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_guava_guava//jar"],
+ runtime_deps = [
+ ":com_google_code_findbugs_jsr305",
+ ":com_google_errorprone_error_prone_annotations",
+ ":com_google_j2objc_j2objc_annotations",
+ ":org_codehaus_mojo_animal_sniffer_annotations",
+ ],
+ )
+
+
+ native.java_library(
+ name = "io_grpc_grpc_all",
+ visibility = ["//visibility:public"],
+ exports = ["@io_grpc_grpc_all//jar"],
+ runtime_deps = [
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_code_findbugs_jsr305",
+ ":com_google_code_gson_gson",
+ ":com_google_errorprone_error_prone_annotations",
+ ":com_google_guava_guava",
+ ":com_google_instrumentation_instrumentation_api",
+ ":com_google_protobuf_nano_protobuf_javanano",
+ ":com_google_protobuf_protobuf_java",
+ ":com_google_protobuf_protobuf_java_util",
+ ":com_squareup_okhttp_okhttp",
+ ":com_squareup_okio_okio",
+ ":io_grpc_grpc_auth",
+ ":io_grpc_grpc_context",
+ ":io_grpc_grpc_core",
+ ":io_grpc_grpc_netty",
+ ":io_grpc_grpc_okhttp",
+ ":io_grpc_grpc_protobuf",
+ ":io_grpc_grpc_protobuf_lite",
+ ":io_grpc_grpc_protobuf_nano",
+ ":io_grpc_grpc_stub",
+ ":io_grpc_grpc_testing",
+ ":io_netty_netty_buffer",
+ ":io_netty_netty_codec",
+ ":io_netty_netty_codec_http",
+ ":io_netty_netty_codec_http2",
+ ":io_netty_netty_codec_socks",
+ ":io_netty_netty_common",
+ ":io_netty_netty_handler",
+ ":io_netty_netty_handler_proxy",
+ ":io_netty_netty_resolver",
+ ":io_netty_netty_transport",
+ ":io_opencensus_opencensus_api",
+ ":io_opencensus_opencensus_contrib_grpc_metrics",
+ ":junit_junit",
+ ":org_hamcrest_hamcrest_core",
+ ":org_mockito_mockito_core",
+ ":org_objenesis_objenesis",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_cloud_google_cloud_core",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_cloud_google_cloud_core//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_gax",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_api_grpc_proto_google_iam_v1",
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_auth_google_auth_library_oauth2_http",
+ ":com_google_http_client_google_http_client",
+ ":com_google_http_client_google_http_client_jackson2",
+ ":com_google_protobuf_protobuf_java",
+ ":com_google_protobuf_protobuf_java_util",
+ ":commons_codec_commons_codec",
+ ":commons_logging_commons_logging",
+ ":joda_time_joda_time",
+ ":org_apache_httpcomponents_httpclient",
+ ":org_apache_httpcomponents_httpcore",
+ ":org_threeten_threetenbp",
+ ],
+ )
+
+
+ native.java_library(
+ name = "com_google_cloud_google_cloud_monitoring",
+ visibility = ["//visibility:public"],
+ exports = ["@com_google_cloud_google_cloud_monitoring//jar"],
+ runtime_deps = [
+ ":com_google_api_api_common",
+ ":com_google_api_gax",
+ ":com_google_api_gax_grpc",
+ ":com_google_api_grpc_proto_google_cloud_monitoring_v3",
+ ":com_google_api_grpc_proto_google_common_protos",
+ ":com_google_api_grpc_proto_google_iam_v1",
+ ":com_google_auth_google_auth_library_credentials",
+ ":com_google_auth_google_auth_library_oauth2_http",
+ ":com_google_cloud_google_cloud_core",
+ ":com_google_cloud_google_cloud_core_grpc",
+ ":com_google_http_client_google_http_client",
+ ":com_google_http_client_google_http_client_jackson2",
+ ":com_google_protobuf_protobuf_java",
+ ":com_google_protobuf_protobuf_java_util",
+ ":commons_codec_commons_codec",
+ ":commons_logging_commons_logging",
+ ":io_grpc_grpc_auth",
+ ":io_grpc_grpc_context",
+ ":io_grpc_grpc_core",
+ ":io_grpc_grpc_netty_shaded",
+ ":io_grpc_grpc_protobuf",
+ ":io_grpc_grpc_stub",
+ ":joda_time_joda_time",
+ ":org_apache_httpcomponents_httpclient",
+ ":org_apache_httpcomponents_httpcore",
+ ":org_threeten_threetenbp",
+ ],
+ )
+
+
diff --git a/examples/pom.xml b/examples/pom.xml
new file mode 100644
index 00000000..5f083126
--- /dev/null
+++ b/examples/pom.xml
@@ -0,0 +1,169 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-examples</artifactId>
+ <packaging>jar</packaging>
+ <version>0.17.0-SNAPSHOT</version><!-- CURRENT_OPENCENSUS_VERSION -->
+ <name>opencensus-examples</name>
+ <url>http://maven.apache.org</url>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <!-- change to the version you want to use. -->
+ <opencensus.version>0.16.1</opencensus.version><!-- LATEST_OPENCENSUS_RELEASE_VERSION -->
+ <grpc.version>1.13.1</grpc.version><!-- CURRENT_GRPC_VERSION -->
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>${opencensus.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-grpc-metrics</artifactId>
+ <version>${opencensus.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-contrib-zpages</artifactId>
+ <version>${opencensus.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-stats-stackdriver</artifactId>
+ <version>${opencensus.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-stats-prometheus</artifactId>
+ <version>${opencensus.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-stackdriver</artifactId>
+ <version>${opencensus.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-logging</artifactId>
+ <version>${opencensus.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-netty</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-protobuf</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.grpc</groupId>
+ <artifactId>grpc-stub</artifactId>
+ <version>${grpc.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient_httpserver</artifactId>
+ <version>0.3.0</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>${opencensus.version}</version>
+ <scope>runtime</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-tcnative-boringssl-static</artifactId>
+ <version>2.0.8.Final</version>
+ <scope>runtime</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <extensions>
+ <extension>
+ <groupId>kr.motd.maven</groupId>
+ <artifactId>os-maven-plugin</artifactId>
+ <version>1.5.0.Final</version>
+ </extension>
+ </extensions>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.7.0</version>
+ <configuration>
+ <source>1.8</source>
+ <target>1.8</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>appassembler-maven-plugin</artifactId>
+ <version>1.10</version>
+ <configuration>
+ <programs>
+ <program>
+ <id>TagContextExample</id>
+ <mainClass>io.opencensus.examples.tags.TagContextExample</mainClass>
+ </program>
+ <program>
+ <id>MultiSpansTracing</id>
+ <mainClass>io.opencensus.examples.trace.MultiSpansTracing</mainClass>
+ </program>
+ <program>
+ <id>MultiSpansScopedTracing</id>
+ <mainClass>io.opencensus.examples.trace.MultiSpansScopedTracing</mainClass>
+ </program>
+ <program>
+ <id>MultiSpansContextTracing</id>
+ <mainClass>io.opencensus.examples.trace.MultiSpansContextTracing</mainClass>
+ </program>
+ <program>
+ <id>ZPagesTester</id>
+ <mainClass>io.opencensus.examples.zpages.ZPagesTester</mainClass>
+ </program>
+ <program>
+ <id>QuickStart</id>
+ <mainClass>io.opencensus.examples.helloworld.QuickStart</mainClass>
+ </program>
+ <program>
+ <id>HelloWorldClient</id>
+ <mainClass>io.opencensus.examples.grpc.helloworld.HelloWorldClient</mainClass>
+ </program>
+ <program>
+ <id>HelloWorldServer</id>
+ <mainClass>io.opencensus.examples.grpc.helloworld.HelloWorldServer</mainClass>
+ </program>
+ </programs>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.xolstice.maven.plugins</groupId>
+ <artifactId>protobuf-maven-plugin</artifactId>
+ <version>0.5.0</version>
+ <configuration>
+ <protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact>
+ <pluginId>grpc-java</pluginId>
+ <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>compile</goal>
+ <goal>compile-custom</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
+
diff --git a/examples/settings.gradle b/examples/settings.gradle
new file mode 100644
index 00000000..310e652f
--- /dev/null
+++ b/examples/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'opencensus-examples'
diff --git a/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldClient.java b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldClient.java
new file mode 100644
index 00000000..30e41633
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldClient.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2018, 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.examples.grpc.helloworld;
+
+import static io.opencensus.examples.grpc.helloworld.HelloWorldUtils.getPortOrDefaultFromArgs;
+import static io.opencensus.examples.grpc.helloworld.HelloWorldUtils.getStringOrDefaultFromArgs;
+
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.StatusRuntimeException;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.grpc.metrics.RpcViews;
+import io.opencensus.contrib.zpages.ZPageHandlers;
+import io.opencensus.exporter.stats.prometheus.PrometheusStatsCollector;
+import io.opencensus.exporter.stats.stackdriver.StackdriverStatsConfiguration;
+import io.opencensus.exporter.stats.stackdriver.StackdriverStatsExporter;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.exporter.trace.stackdriver.StackdriverTraceConfiguration;
+import io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** A simple client that requests a greeting from the {@link HelloWorldServer}. */
+public class HelloWorldClient {
+ private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName());
+
+ private static final Tracer tracer = Tracing.getTracer();
+
+ private final ManagedChannel channel;
+ private final GreeterGrpc.GreeterBlockingStub blockingStub;
+
+ /** Construct client connecting to HelloWorld server at {@code host:port}. */
+ public HelloWorldClient(String host, int port) {
+ this(
+ ManagedChannelBuilder.forAddress(host, port)
+ // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
+ // needing certificates.
+ .usePlaintext(true)
+ .build());
+ }
+
+ /** Construct client for accessing RouteGuide server using the existing channel. */
+ HelloWorldClient(ManagedChannel channel) {
+ this.channel = channel;
+ blockingStub = GreeterGrpc.newBlockingStub(channel);
+ }
+
+ public void shutdown() throws InterruptedException {
+ channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
+ }
+
+ /** Say hello to server. */
+ public void greet(String name) {
+ logger.info("Will try to greet " + name + " ...");
+ HelloRequest request = HelloRequest.newBuilder().setName(name).build();
+ HelloReply response;
+
+ SpanBuilder spanBuilder =
+ tracer.spanBuilder("client").setRecordEvents(true).setSampler(Samplers.alwaysSample());
+ try (Scope scope = spanBuilder.startScopedSpan()) {
+ tracer.getCurrentSpan().addAnnotation("Saying Hello to Server.");
+ response = blockingStub.sayHello(request);
+ tracer.getCurrentSpan().addAnnotation("Received response from Server.");
+ } catch (StatusRuntimeException e) {
+ tracer
+ .getCurrentSpan()
+ .setStatus(
+ CanonicalCode.valueOf(e.getStatus().getCode().name())
+ .toStatus()
+ .withDescription(e.getMessage()));
+ logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
+ return;
+ }
+ logger.info("Greeting: " + response.getMessage());
+ }
+
+ /**
+ * Greet server. If provided, the first element of {@code args} is the name to use in the
+ * greeting.
+ */
+ public static void main(String[] args) throws IOException, InterruptedException {
+ // Add final keyword to pass checkStyle.
+ final String user = getStringOrDefaultFromArgs(args, 0, "world");
+ final String host = getStringOrDefaultFromArgs(args, 1, "localhost");
+ final int serverPort = getPortOrDefaultFromArgs(args, 2, 50051);
+ final String cloudProjectId = getStringOrDefaultFromArgs(args, 3, null);
+ final int zPagePort = getPortOrDefaultFromArgs(args, 4, 3001);
+
+ // Registers all RPC views.
+ RpcViews.registerAllViews();
+
+ // Starts a HTTP server and registers all Zpages to it.
+ ZPageHandlers.startHttpServerAndRegisterAll(zPagePort);
+ logger.info("ZPages server starts at localhost:" + zPagePort);
+
+ // Registers logging trace exporter.
+ LoggingTraceExporter.register();
+
+ // Registers Stackdriver exporters.
+ if (cloudProjectId != null) {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder().setProjectId(cloudProjectId).build());
+ StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder()
+ .setProjectId(cloudProjectId)
+ .setExportInterval(Duration.create(15, 0))
+ .build());
+ }
+
+ // Register Prometheus exporters and export metrics to a Prometheus HTTPServer.
+ PrometheusStatsCollector.createAndRegister();
+
+ HelloWorldClient client = new HelloWorldClient(host, serverPort);
+ try {
+ client.greet(user);
+ } finally {
+ client.shutdown();
+ }
+
+ logger.info("Client sleeping, ^C to exit. Meanwhile you can view stats and spans on zpages.");
+ while (true) {
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ logger.info("Exiting HelloWorldClient...");
+ }
+ }
+ }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldServer.java b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldServer.java
new file mode 100644
index 00000000..15a0a896
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldServer.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2018, 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.examples.grpc.helloworld;
+
+import static io.opencensus.examples.grpc.helloworld.HelloWorldUtils.getPortOrDefaultFromArgs;
+import static io.opencensus.examples.grpc.helloworld.HelloWorldUtils.getStringOrDefaultFromArgs;
+
+import com.google.common.collect.ImmutableMap;
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+import io.grpc.stub.StreamObserver;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.grpc.metrics.RpcViews;
+import io.opencensus.contrib.zpages.ZPageHandlers;
+import io.opencensus.exporter.stats.prometheus.PrometheusStatsCollector;
+import io.opencensus.exporter.stats.stackdriver.StackdriverStatsConfiguration;
+import io.opencensus.exporter.stats.stackdriver.StackdriverStatsExporter;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.exporter.trace.stackdriver.StackdriverTraceConfiguration;
+import io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import io.prometheus.client.exporter.HTTPServer;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/** Server that manages startup/shutdown of a {@code Greeter} server. */
+public class HelloWorldServer {
+ private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName());
+
+ private static final Tracer tracer = Tracing.getTracer();
+
+ private final int serverPort;
+ private Server server;
+
+ private HelloWorldServer(int serverPort) {
+ this.serverPort = serverPort;
+ }
+
+ // A helper function that performs some work in its own Span.
+ private static void performWork(Span parent) {
+ SpanBuilder spanBuilder =
+ tracer
+ .spanBuilderWithExplicitParent("internal_work", parent)
+ .setRecordEvents(true)
+ .setSampler(Samplers.alwaysSample());
+ try (Scope scope = spanBuilder.startScopedSpan()) {
+ Span span = tracer.getCurrentSpan();
+ span.putAttribute("my_attribute", AttributeValue.stringAttributeValue("blue"));
+ span.addAnnotation("Performing work.");
+ sleepFor(20); // Working hard here.
+ span.addAnnotation("Done work.");
+ }
+ }
+
+ private static void sleepFor(int milliseconds) {
+ try {
+ Thread.sleep(milliseconds);
+ } catch (InterruptedException e) {
+ Span span = tracer.getCurrentSpan();
+ span.addAnnotation("Exception thrown when performing work " + e.getMessage());
+ span.setStatus(Status.UNKNOWN);
+ }
+ }
+
+ private void start() throws IOException {
+ server = ServerBuilder.forPort(serverPort).addService(new GreeterImpl()).build().start();
+ logger.info("Server started, listening on " + serverPort);
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread() {
+ @Override
+ public void run() {
+ // Use stderr here since the logger may have been reset by its JVM shutdown hook.
+ System.err.println("*** shutting down gRPC server since JVM is shutting down");
+ HelloWorldServer.this.stop();
+ System.err.println("*** server shut down");
+ }
+ });
+ }
+
+ private void stop() {
+ if (server != null) {
+ server.shutdown();
+ }
+ }
+
+ /** Await termination on the main thread since the grpc library uses daemon threads. */
+ private void blockUntilShutdown() throws InterruptedException {
+ if (server != null) {
+ server.awaitTermination();
+ }
+ }
+
+ /** Main launches the server from the command line. */
+ public static void main(String[] args) throws IOException, InterruptedException {
+ // Add final keyword to pass checkStyle.
+ final int serverPort = getPortOrDefaultFromArgs(args, 0, 50051);
+ final String cloudProjectId = getStringOrDefaultFromArgs(args, 1, null);
+ final int zPagePort = getPortOrDefaultFromArgs(args, 2, 3000);
+ final int prometheusPort = getPortOrDefaultFromArgs(args, 3, 9090);
+
+ // Registers all RPC views.
+ RpcViews.registerAllViews();
+
+ // Registers logging trace exporter.
+ LoggingTraceExporter.register();
+
+ // Starts a HTTP server and registers all Zpages to it.
+ ZPageHandlers.startHttpServerAndRegisterAll(zPagePort);
+ logger.info("ZPages server starts at localhost:" + zPagePort);
+
+ // Registers Stackdriver exporters.
+ if (cloudProjectId != null) {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder().setProjectId(cloudProjectId).build());
+ StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder()
+ .setProjectId(cloudProjectId)
+ .setExportInterval(Duration.create(15, 0))
+ .build());
+ }
+
+ // Register Prometheus exporters and export metrics to a Prometheus HTTPServer.
+ PrometheusStatsCollector.createAndRegister();
+ HTTPServer prometheusServer = new HTTPServer(prometheusPort, true);
+
+ // Start the RPC server. You shouldn't see any output from gRPC before this.
+ logger.info("gRPC starting.");
+ final HelloWorldServer server = new HelloWorldServer(serverPort);
+ server.start();
+ server.blockUntilShutdown();
+ }
+
+ static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
+
+ @Override
+ public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
+ Span span = tracer.getCurrentSpan();
+ span.putAttribute("my_attribute", AttributeValue.stringAttributeValue("red"));
+ span.addAnnotation(
+ "Constructing greeting.",
+ ImmutableMap.of(
+ "name", AttributeValue.stringAttributeValue(req.getName()),
+ "name length", AttributeValue.longAttributeValue(req.getName().length())));
+ sleepFor(10);
+ performWork(span);
+ span.addAnnotation("Sleeping.");
+ sleepFor(30);
+ HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
+ responseObserver.onNext(reply);
+ responseObserver.onCompleted();
+ logger.info("SayHello RPC handled.");
+ }
+ }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldUtils.java b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldUtils.java
new file mode 100644
index 00000000..55d6c225
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/grpc/helloworld/HelloWorldUtils.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018, 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.examples.grpc.helloworld;
+
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/** Util methods. */
+final class HelloWorldUtils {
+
+ private static final Logger logger = Logger.getLogger(HelloWorldUtils.class.getName());
+
+ static int getPortOrDefaultFromArgs(String[] args, int index, int defaultPort) {
+ int portNumber = defaultPort;
+ if (index < args.length) {
+ try {
+ portNumber = Integer.parseInt(args[index]);
+ } catch (NumberFormatException e) {
+ logger.warning(
+ String.format("Port %s is invalid, use default port %d.", args[index], defaultPort));
+ }
+ }
+ return portNumber;
+ }
+
+ static String getStringOrDefaultFromArgs(
+ String[] args, int index, @Nullable String defaultString) {
+ String s = defaultString;
+ if (index < args.length) {
+ s = args[index];
+ }
+ return s;
+ }
+
+ private HelloWorldUtils() {}
+}
diff --git a/examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java b/examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java
new file mode 100644
index 00000000..c71e0f3e
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/helloworld/QuickStart.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018, 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.examples.helloworld;
+
+import io.opencensus.common.Scope;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.Tags;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Random;
+import java.util.logging.Logger;
+
+/** Simple program that collects data for video size. */
+public final class QuickStart {
+
+ private static final Logger logger = Logger.getLogger(QuickStart.class.getName());
+
+ private static final Tagger tagger = Tags.getTagger();
+ private static final ViewManager viewManager = Stats.getViewManager();
+ private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
+ private static final Tracer tracer = Tracing.getTracer();
+
+ // frontendKey allows us to break down the recorded data.
+ private static final TagKey FRONTEND_KEY = TagKey.create("my.org/keys/frontend");
+
+ // videoSize will measure the size of processed videos.
+ private static final MeasureLong VIDEO_SIZE =
+ MeasureLong.create("my.org/measure/video_size", "size of processed videos", "By");
+
+ private static final long MiB = 1 << 20;
+
+ // Create view to see the processed video size distribution broken down by frontend.
+ // The view has bucket boundaries (0, 16 * MiB, 65536 * MiB) that will group measure
+ // values into histogram buckets.
+ private static final View.Name VIDEO_SIZE_VIEW_NAME = View.Name.create("my.org/views/video_size");
+ private static final View VIDEO_SIZE_VIEW =
+ View.create(
+ VIDEO_SIZE_VIEW_NAME,
+ "processed video size over time",
+ VIDEO_SIZE,
+ Aggregation.Distribution.create(
+ BucketBoundaries.create(Arrays.asList(0.0, 16.0 * MiB, 256.0 * MiB))),
+ Collections.singletonList(FRONTEND_KEY));
+
+ /** Main launcher for the QuickStart example. */
+ public static void main(String[] args) throws InterruptedException {
+ TagContextBuilder tagContextBuilder =
+ tagger.currentBuilder().put(FRONTEND_KEY, TagValue.create("mobile-ios9.3.5"));
+ SpanBuilder spanBuilder =
+ tracer
+ .spanBuilder("my.org/ProcessVideo")
+ .setRecordEvents(true)
+ .setSampler(Samplers.alwaysSample());
+ viewManager.registerView(VIDEO_SIZE_VIEW);
+ LoggingTraceExporter.register();
+
+ // Process video.
+ // Record the processed video size.
+ try (Scope scopedTags = tagContextBuilder.buildScoped();
+ Scope scopedSpan = spanBuilder.startScopedSpan()) {
+ tracer.getCurrentSpan().addAnnotation("Start processing video.");
+ // Sleep for [0,10] milliseconds to fake work.
+ Thread.sleep(new Random().nextInt(10) + 1);
+ statsRecorder.newMeasureMap().put(VIDEO_SIZE, 25 * MiB).record();
+ tracer.getCurrentSpan().addAnnotation("Finished processing video.");
+ } catch (Exception e) {
+ tracer.getCurrentSpan().addAnnotation("Exception thrown when processing video.");
+ tracer.getCurrentSpan().setStatus(Status.UNKNOWN);
+ logger.severe(e.getMessage());
+ }
+
+ logger.info("Wait longer than the reporting duration...");
+ // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+ // TODO(songya): remove the gap once we add a shutdown hook for exporting unflushed spans.
+ Thread.sleep(5100);
+ ViewData viewData = viewManager.getView(VIDEO_SIZE_VIEW_NAME);
+ logger.info(
+ String.format("Recorded stats for %s:\n %s", VIDEO_SIZE_VIEW_NAME.asString(), viewData));
+ }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/tags/TagContextExample.java b/examples/src/main/java/io/opencensus/examples/tags/TagContextExample.java
new file mode 100644
index 00000000..727c5fbb
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/tags/TagContextExample.java
@@ -0,0 +1,77 @@
+/*
+ * 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.examples.tags;
+
+import io.opencensus.common.Scope;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.Tags;
+
+/** Simple program that uses {@link TagContext}. */
+public class TagContextExample {
+
+ 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 String UNIT = "1";
+ private static final MeasureDouble M1 = MeasureDouble.create("m1", "1st test metric", UNIT);
+ private static final MeasureDouble M2 = MeasureDouble.create("m2", "2nd test metric", UNIT);
+
+ private static final Tagger tagger = Tags.getTagger();
+ private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
+
+ private TagContextExample() {}
+
+ /**
+ * Main method.
+ *
+ * @param args the main arguments.
+ */
+ public static void main(String[] args) {
+ System.out.println("Hello Stats World");
+ System.out.println("Default Tags: " + tagger.empty());
+ System.out.println("Current Tags: " + tagger.getCurrentTagContext());
+ TagContext tags1 = tagger.emptyBuilder().put(K1, V1).put(K2, V2).build();
+ try (Scope scopedTagCtx1 = tagger.withTagContext(tags1)) {
+ System.out.println(" Current Tags: " + tagger.getCurrentTagContext());
+ System.out.println(
+ " Current == Default + tags1: " + tagger.getCurrentTagContext().equals(tags1));
+ TagContext tags2 = tagger.toBuilder(tags1).put(K3, V3).put(K4, V4).build();
+ try (Scope scopedTagCtx2 = tagger.withTagContext(tags2)) {
+ System.out.println(" Current Tags: " + tagger.getCurrentTagContext());
+ System.out.println(
+ " Current == Default + tags1 + tags2: "
+ + tagger.getCurrentTagContext().equals(tags2));
+ statsRecorder.newMeasureMap().put(M1, 0.2).put(M2, 0.4).record();
+ }
+ }
+ System.out.println(
+ "Current == Default: " + tagger.getCurrentTagContext().equals(tagger.empty()));
+ }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/trace/MultiSpansContextTracing.java b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansContextTracing.java
new file mode 100644
index 00000000..c8df144f
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansContextTracing.java
@@ -0,0 +1,89 @@
+/*
+ * 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.examples.trace;
+
+import static io.opencensus.examples.trace.Utils.sleep;
+
+import io.opencensus.common.Scope;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.samplers.Samplers;
+
+/**
+ * Example showing how to create a child {@link Span}, install it to the current context and add
+ * annotations.
+ */
+public final class MultiSpansContextTracing {
+ // Per class Tracer.
+ private static final Tracer tracer = Tracing.getTracer();
+
+ private MultiSpansContextTracing() {}
+
+ private static void doSomeOtherWork() {
+ tracer.getCurrentSpan().addAnnotation("Annotation to the child Span");
+ }
+
+ private static void doSomeMoreWork() {
+ // Create a child Span of the current Span.
+ Span span = tracer.spanBuilder("MyChildSpan").startSpan();
+ try (Scope ws = tracer.withSpan(span)) {
+ doSomeOtherWork();
+ }
+ span.end();
+ }
+
+ private static void doWork() {
+ tracer.getCurrentSpan().addAnnotation("Annotation to the root Span before child is created.");
+ doSomeMoreWork();
+ tracer.getCurrentSpan().addAnnotation("Annotation to the root Span after child is ended.");
+ }
+
+ /**
+ * Main method.
+ *
+ * @param args the main arguments.
+ */
+ public static void main(String[] args) {
+
+ // WARNING: Be careful before you set sampler value to always sample, especially in
+ // production environment. Trace data is often very large in size and is expensive to
+ // collect. This is why rather than collecting traces for every request(i.e. alwaysSample),
+ // downsampling is prefered.
+ //
+ // By default, OpenCensus provides a probabilistic sampler that will trace once in every
+ // 10,000 requests, that's why if default probabilistic sampler is used
+ // you might not see trace data printed or exported and this is expected behavior.
+
+ TraceConfig traceConfig = Tracing.getTraceConfig();
+ traceConfig.updateActiveTraceParams(
+ traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
+
+ LoggingTraceExporter.register();
+ Span span = tracer.spanBuilderWithExplicitParent("MyRootSpan", null).startSpan();
+ try (Scope ws = tracer.withSpan(span)) {
+ doWork();
+ }
+ span.end();
+
+ // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+ // Spans are exported every 5 seconds
+ sleep(5100);
+ }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/trace/MultiSpansScopedTracing.java b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansScopedTracing.java
new file mode 100644
index 00000000..5cfc9dfc
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansScopedTracing.java
@@ -0,0 +1,85 @@
+/*
+ * 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.examples.trace;
+
+import static io.opencensus.examples.trace.Utils.sleep;
+
+import io.opencensus.common.Scope;
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.samplers.Samplers;
+
+/**
+ * Example showing how to create a child {@link Span} using scoped Spans, install it in the current
+ * context, and add annotations.
+ */
+public final class MultiSpansScopedTracing {
+ // Per class Tracer.
+ private static final Tracer tracer = Tracing.getTracer();
+
+ private MultiSpansScopedTracing() {}
+
+ private static void doSomeOtherWork() {
+ tracer.getCurrentSpan().addAnnotation("Annotation to the child Span");
+ }
+
+ private static void doSomeMoreWork() {
+ // Create a child Span of the current Span.
+ try (Scope ss = tracer.spanBuilder("MyChildSpan").startScopedSpan()) {
+ doSomeOtherWork();
+ }
+ }
+
+ private static void doWork() {
+ tracer.getCurrentSpan().addAnnotation("Annotation to the root Span before child is created.");
+ doSomeMoreWork();
+ tracer.getCurrentSpan().addAnnotation("Annotation to the root Span after child is ended.");
+ }
+
+ /**
+ * Main method.
+ *
+ * @param args the main arguments.
+ */
+ public static void main(String[] args) {
+
+ // WARNING: Be careful before you set sampler value to always sample, especially in
+ // production environment. Trace data is often very large in size and is expensive to
+ // collect. This is why rather than collecting traces for every request(i.e. alwaysSample),
+ // downsampling is prefered.
+ //
+ // By default, OpenCensus provides a probabilistic sampler that will trace once in every
+ // 10,000 requests, that's why if default probabilistic sampler is used
+ // you might not see trace data printed or exported and this is expected behavior.
+
+ TraceConfig traceConfig = Tracing.getTraceConfig();
+ traceConfig.updateActiveTraceParams(
+ traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
+
+ LoggingTraceExporter.register();
+ try (Scope ss = tracer.spanBuilderWithExplicitParent("MyRootSpan", null).startScopedSpan()) {
+ doWork();
+ }
+
+ // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+ // Spans are exported every 5 seconds
+ sleep(5100);
+ }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/trace/MultiSpansTracing.java b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansTracing.java
new file mode 100644
index 00000000..fae4e3ff
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/trace/MultiSpansTracing.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.examples.trace;
+
+import static io.opencensus.examples.trace.Utils.sleep;
+
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.samplers.Samplers;
+
+/** Example showing how to directly create a child {@link Span} and add annotations. */
+public final class MultiSpansTracing {
+ // Per class Tracer.
+ private static final Tracer tracer = Tracing.getTracer();
+
+ private MultiSpansTracing() {}
+
+ private static void doWork() {
+ Span rootSpan = tracer.spanBuilderWithExplicitParent("MyRootSpan", null).startSpan();
+ rootSpan.addAnnotation("Annotation to the root Span before child is created.");
+ Span childSpan = tracer.spanBuilderWithExplicitParent("MyChildSpan", rootSpan).startSpan();
+ childSpan.addAnnotation("Annotation to the child Span");
+ childSpan.end();
+ rootSpan.addAnnotation("Annotation to the root Span after child is ended.");
+ rootSpan.end();
+ }
+
+ /**
+ * Main method.
+ *
+ * @param args the main arguments.
+ */
+ public static void main(String[] args) {
+
+ // WARNING: Be careful before you set sampler value to always sample, especially in
+ // production environment. Trace data is often very large in size and is expensive to
+ // collect. This is why rather than collecting traces for every request(i.e. alwaysSample),
+ // downsampling is prefered.
+ //
+ // By default, OpenCensus provides a probabilistic sampler that will trace once in every
+ // 10,000 requests, that's why if default probabilistic sampler is used
+ // you might not see trace data printed or exported and this is expected behavior.
+
+ TraceConfig traceConfig = Tracing.getTraceConfig();
+ traceConfig.updateActiveTraceParams(
+ traceConfig.getActiveTraceParams().toBuilder().setSampler(Samplers.alwaysSample()).build());
+
+ LoggingTraceExporter.register();
+ doWork();
+
+ // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+ // Spans are exported every 5 seconds
+ sleep(5100);
+ }
+}
diff --git a/examples/src/main/java/io/opencensus/examples/trace/Utils.java b/examples/src/main/java/io/opencensus/examples/trace/Utils.java
new file mode 100644
index 00000000..9f0338af
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/trace/Utils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018, 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.examples.trace;
+
+import java.util.logging.Logger;
+
+/** Util methods. */
+final class Utils {
+
+ private static final Logger logger = Logger.getLogger(Utils.class.getName());
+
+ static void sleep(int ms) {
+ // A helper to avoid try-catch when invoking Thread.sleep so that
+ // sleeps can be succinct and not permeated by exception handling.
+ try {
+ Thread.sleep(ms);
+ } catch (Exception e) {
+ logger.warning((String.format("Failed to sleep for %dms. Exception: %s", ms, e)));
+ }
+ }
+
+ private Utils() {}
+}
diff --git a/examples/src/main/java/io/opencensus/examples/zpages/ZPagesTester.java b/examples/src/main/java/io/opencensus/examples/zpages/ZPagesTester.java
new file mode 100644
index 00000000..282b40ec
--- /dev/null
+++ b/examples/src/main/java/io/opencensus/examples/zpages/ZPagesTester.java
@@ -0,0 +1,108 @@
+/*
+ * 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.examples.zpages;
+
+import io.opencensus.common.Scope;
+import io.opencensus.contrib.grpc.metrics.RpcMeasureConstants;
+import io.opencensus.contrib.grpc.metrics.RpcViews;
+import io.opencensus.contrib.zpages.ZPageHandlers;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.Tags;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+
+/** Testing only class for the UI. */
+public class ZPagesTester {
+
+ private ZPagesTester() {}
+
+ private static final Tagger tagger = Tags.getTagger();
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
+
+ private static final String SPAN_NAME = "ExampleSpan";
+ private static final TagValue METHOD = TagValue.create("ExampleMethod");
+
+ private static void recordExampleData() throws InterruptedException {
+ Tracing.getExportComponent()
+ .getSampledSpanStore()
+ .registerSpanNamesForCollection(Collections.singletonList(SPAN_NAME));
+ RpcViews.registerAllViews(); // Use old RPC constants to get interval stats.
+ SpanBuilder spanBuilder =
+ tracer.spanBuilder(SPAN_NAME).setRecordEvents(true).setSampler(Samplers.alwaysSample());
+
+ try (Scope scope = spanBuilder.startScopedSpan()) {
+ tracer.getCurrentSpan().addAnnotation("Starts recording.");
+ MeasureMap measureMap =
+ statsRecorder
+ .newMeasureMap()
+ // Client measurements.
+ .put(RpcMeasureConstants.RPC_CLIENT_STARTED_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_CLIENT_FINISHED_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_CLIENT_ROUNDTRIP_LATENCY, 1.0)
+ .put(RpcMeasureConstants.RPC_CLIENT_REQUEST_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_CLIENT_RESPONSE_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_CLIENT_REQUEST_BYTES, 1e5)
+ .put(RpcMeasureConstants.RPC_CLIENT_RESPONSE_BYTES, 1e5)
+ .put(RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_REQUEST_BYTES, 1e5)
+ .put(RpcMeasureConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES, 1e5)
+ // Server measurements.
+ .put(RpcMeasureConstants.RPC_SERVER_STARTED_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_SERVER_FINISHED_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_SERVER_SERVER_LATENCY, 1.0)
+ .put(RpcMeasureConstants.RPC_SERVER_REQUEST_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_SERVER_RESPONSE_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_SERVER_REQUEST_BYTES, 1e5)
+ .put(RpcMeasureConstants.RPC_SERVER_RESPONSE_BYTES, 1e5)
+ .put(RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_REQUEST_BYTES, 1e5)
+ .put(RpcMeasureConstants.RPC_SERVER_UNCOMPRESSED_RESPONSE_BYTES, 1e5);
+ measureMap.record(
+ tagger
+ .currentBuilder()
+ .put(RpcMeasureConstants.RPC_STATUS, TagValue.create("OK"))
+ .put(RpcMeasureConstants.RPC_METHOD, METHOD)
+ .build());
+ MeasureMap measureMapErrors =
+ statsRecorder
+ .newMeasureMap()
+ .put(RpcMeasureConstants.RPC_CLIENT_ERROR_COUNT, 1)
+ .put(RpcMeasureConstants.RPC_SERVER_ERROR_COUNT, 1);
+ measureMapErrors.record(
+ tagger
+ .currentBuilder()
+ .put(RpcMeasureConstants.RPC_STATUS, TagValue.create("UNKNOWN"))
+ .put(RpcMeasureConstants.RPC_METHOD, METHOD)
+ .build());
+
+ Thread.sleep(200); // sleep for fake work.
+ tracer.getCurrentSpan().addAnnotation("Finish recording.");
+ }
+ }
+
+ /** Main method. */
+ public static void main(String[] args) throws Exception {
+ ZPageHandlers.startHttpServerAndRegisterAll(8080);
+ recordExampleData();
+ }
+}
diff --git a/examples/src/main/proto/helloworld.proto b/examples/src/main/proto/helloworld.proto
new file mode 100644
index 00000000..1bd79300
--- /dev/null
+++ b/examples/src/main/proto/helloworld.proto
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.opencensus.examples.grpc.helloworld";
+option java_outer_classname = "HelloWorldProto";
+
+package helloworld;
+
+// The greeting service definition.
+service Greeter {
+ // Sends a greeting
+ rpc SayHello (HelloRequest) returns (HelloReply) {}
+}
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+}
+
+// The response message containing the greetings
+message HelloReply {
+ string message = 1;
+}
diff --git a/exporters/stats/prometheus/README.md b/exporters/stats/prometheus/README.md
new file mode 100644
index 00000000..fa19efc9
--- /dev/null
+++ b/exporters/stats/prometheus/README.md
@@ -0,0 +1,81 @@
+# OpenCensus Prometheus Stats Exporter
+
+The *OpenCensus Prometheus Stats Exporter* is a stats exporter that exports data to
+Prometheus. [Prometheus](https://prometheus.io/) is an open-source systems monitoring and alerting
+toolkit originally built at [SoundCloud](https://soundcloud.com/).
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you need to install, configure and start Prometheus first. Follow the
+instructions [here](https://prometheus.io/docs/introduction/first_steps/).
+
+### Hello "Prometheus Stats"
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-stats-prometheus</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-stats-prometheus:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) {
+ // Creates a PrometheusStatsCollector and registers it to the default Prometheus registry.
+ PrometheusStatsCollector.createAndRegister();
+
+ // Uses a simple Prometheus HTTPServer to export metrics.
+ // You can use a Prometheus PushGateway instead, though that's discouraged by Prometheus:
+ // https://prometheus.io/docs/practices/pushing/#should-i-be-using-the-pushgateway.
+ io.prometheus.client.exporter.HTTPServer server =
+ new HTTPServer(/*host*/ "localhost", /*port*/ 9091, /*daemon*/ true);
+
+ // Your code here.
+ // ...
+ }
+}
+```
+
+In this example, you should be able to see all the OpenCensus Prometheus metrics by visiting
+localhost:9091/metrics. Every time when you visit localhost:9091/metrics, the metrics will be
+collected from OpenCensus library and refreshed.
+
+#### Exporting
+
+After collecting stats from OpenCensus, there are multiple options for exporting them.
+See [Exporting via HTTP](https://github.com/prometheus/client_java#http), [Exporting to a Pushgateway](https://github.com/prometheus/client_java#exporting-to-a-pushgateway)
+and [Bridges](https://github.com/prometheus/client_java#bridges).
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
diff --git a/exporters/stats/prometheus/build.gradle b/exporters/stats/prometheus/build.gradle
new file mode 100644
index 00000000..fe8563c4
--- /dev/null
+++ b/exporters/stats/prometheus/build.gradle
@@ -0,0 +1,19 @@
+description = 'OpenCensus Stats Prometheus Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ libraries.guava,
+ libraries.prometheus_simpleclient
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+} \ No newline at end of file
diff --git a/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java
new file mode 100644
index 00000000..288813d3
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtils.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import static io.prometheus.client.Collector.doubleToGoString;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+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.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Util methods to convert OpenCensus Stats data models to Prometheus data models.
+ *
+ * <p>Each OpenCensus {@link View} will be converted to a Prometheus {@link MetricFamilySamples}
+ * with no {@link Sample}s, and is used for registering Prometheus {@code Metric}s. Only {@code
+ * Cumulative} views are supported. All views are under namespace "opencensus".
+ *
+ * <p>{@link Aggregation} will be converted to a corresponding Prometheus {@link Type}. {@link Sum}
+ * will be {@link Type#UNTYPED}, {@link Count} will be {@link Type#COUNTER}, {@link
+ * Aggregation.Mean} will be {@link Type#SUMMARY}, {@link Aggregation.LastValue} will be {@link
+ * Type#GAUGE} and {@link Distribution} will be {@link Type#HISTOGRAM}. Please note we cannot set
+ * bucket boundaries for custom {@link Type#HISTOGRAM}.
+ *
+ * <p>Each OpenCensus {@link ViewData} will be converted to a Prometheus {@link
+ * MetricFamilySamples}, and each {@code Row} of the {@link ViewData} will be converted to
+ * Prometheus {@link Sample}s.
+ *
+ * <p>{@link SumDataDouble}, {@link SumDataLong}, {@link LastValueDataDouble}, {@link
+ * LastValueDataLong} and {@link CountData} will be converted to a single {@link Sample}. {@link
+ * AggregationData.MeanData} will be converted to two {@link Sample}s sum and count. {@link
+ * DistributionData} will be converted to a list of {@link Sample}s that have the sum, count and
+ * histogram buckets.
+ *
+ * <p>{@link TagKey} and {@link TagValue} will be converted to Prometheus {@code LabelName} and
+ * {@code LabelValue}. {@code Null} {@link TagValue} will be converted to an empty string.
+ *
+ * <p>Please note that Prometheus Metric and Label name can only have alphanumeric characters and
+ * underscore. All other characters will be sanitized by underscores.
+ */
+@SuppressWarnings("deprecation")
+final class PrometheusExportUtils {
+
+ @VisibleForTesting static final String SAMPLE_SUFFIX_BUCKET = "_bucket";
+ @VisibleForTesting static final String SAMPLE_SUFFIX_COUNT = "_count";
+ @VisibleForTesting static final String SAMPLE_SUFFIX_SUM = "_sum";
+ @VisibleForTesting static final String LABEL_NAME_BUCKET_BOUND = "le";
+
+ private static final Function<Object, Type> TYPE_UNTYPED_FUNCTION =
+ Functions.returnConstant(Type.UNTYPED);
+ private static final Function<Object, Type> TYPE_COUNTER_FUNCTION =
+ Functions.returnConstant(Type.COUNTER);
+ private static final Function<Object, Type> TYPE_HISTOGRAM_FUNCTION =
+ Functions.returnConstant(Type.HISTOGRAM);
+ private static final Function<Object, Type> TYPE_GAUGE_FUNCTION =
+ Functions.returnConstant(Type.GAUGE);
+
+ // Converts a ViewData to a Prometheus MetricFamilySamples.
+ static MetricFamilySamples createMetricFamilySamples(ViewData viewData) {
+ View view = viewData.getView();
+ String name = Collector.sanitizeMetricName(view.getName().asString());
+ Type type = getType(view.getAggregation(), view.getWindow());
+ List<String> labelNames = convertToLabelNames(view.getColumns());
+ List<Sample> samples = Lists.newArrayList();
+ for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ viewData.getAggregationMap().entrySet()) {
+ samples.addAll(
+ getSamples(name, labelNames, entry.getKey(), entry.getValue(), view.getAggregation()));
+ }
+ return new MetricFamilySamples(name, type, view.getDescription(), samples);
+ }
+
+ // Converts a View to a Prometheus MetricFamilySamples.
+ // Used only for Prometheus metric registry, should not contain any actual samples.
+ static MetricFamilySamples createDescribableMetricFamilySamples(View view) {
+ String name = Collector.sanitizeMetricName(view.getName().asString());
+ Type type = getType(view.getAggregation(), view.getWindow());
+ List<String> labelNames = convertToLabelNames(view.getColumns());
+ if (containsDisallowedLeLabelForHistogram(labelNames, type)) {
+ throw new IllegalStateException(
+ "Prometheus Histogram cannot have a label named 'le', "
+ + "because it is a reserved label for bucket boundaries. "
+ + "Please remove this tag key from your view.");
+ }
+ return new MetricFamilySamples(
+ name, type, view.getDescription(), Collections.<Sample>emptyList());
+ }
+
+ @VisibleForTesting
+ static Type getType(Aggregation aggregation, View.AggregationWindow window) {
+ if (!(window instanceof View.AggregationWindow.Cumulative)) {
+ return Type.UNTYPED;
+ }
+ return aggregation.match(
+ TYPE_UNTYPED_FUNCTION, // SUM
+ TYPE_COUNTER_FUNCTION, // COUNT
+ TYPE_HISTOGRAM_FUNCTION, // DISTRIBUTION
+ TYPE_GAUGE_FUNCTION, // LAST VALUE
+ new Function<Aggregation, Type>() {
+ @Override
+ public Type apply(Aggregation arg) {
+ if (arg instanceof Aggregation.Mean) {
+ return Type.SUMMARY;
+ }
+ return Type.UNTYPED;
+ }
+ });
+ }
+
+ // Converts a row in ViewData (a.k.a Entry<List<TagValue>, AggregationData>) to a list of
+ // Prometheus Samples.
+ @VisibleForTesting
+ static List<Sample> getSamples(
+ final String name,
+ final List<String> labelNames,
+ List</*@Nullable*/ TagValue> tagValues,
+ AggregationData aggregationData,
+ final Aggregation aggregation) {
+ Preconditions.checkArgument(
+ labelNames.size() == tagValues.size(), "Label names and tag values have different sizes.");
+ final List<Sample> samples = Lists.newArrayList();
+ final List<String> labelValues = new ArrayList<String>(tagValues.size());
+ for (TagValue tagValue : tagValues) {
+ String labelValue = tagValue == null ? "" : tagValue.asString();
+ labelValues.add(labelValue);
+ }
+
+ aggregationData.match(
+ new Function<SumDataDouble, Void>() {
+ @Override
+ public Void apply(SumDataDouble arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getSum()));
+ return null;
+ }
+ },
+ new Function<SumDataLong, Void>() {
+ @Override
+ public Void apply(SumDataLong arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getSum()));
+ return null;
+ }
+ },
+ new Function<CountData, Void>() {
+ @Override
+ public Void apply(CountData arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getCount()));
+ return null;
+ }
+ },
+ new Function<DistributionData, Void>() {
+ @Override
+ public Void apply(DistributionData arg) {
+ // For histogram buckets, manually add the bucket boundaries as "le" labels. See
+ // https://github.com/prometheus/client_java/commit/ed184d8e50c82e98bb2706723fff764424840c3a#diff-c505abbde72dd6bf36e89917b3469404R241
+ @SuppressWarnings("unchecked")
+ Distribution distribution = (Distribution) aggregation;
+ List<Double> boundaries = distribution.getBucketBoundaries().getBoundaries();
+ List<String> labelNamesWithLe = new ArrayList<String>(labelNames);
+ labelNamesWithLe.add(LABEL_NAME_BUCKET_BOUND);
+ long cumulativeCount = 0;
+ for (int i = 0; i < arg.getBucketCounts().size(); i++) {
+ List<String> labelValuesWithLe = new ArrayList<String>(labelValues);
+ // The label value of "le" is the upper inclusive bound.
+ // For the last bucket, it should be "+Inf".
+ String bucketBoundary =
+ doubleToGoString(
+ i < boundaries.size() ? boundaries.get(i) : Double.POSITIVE_INFINITY);
+ labelValuesWithLe.add(bucketBoundary);
+ cumulativeCount += arg.getBucketCounts().get(i);
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_BUCKET,
+ labelNamesWithLe,
+ labelValuesWithLe,
+ cumulativeCount));
+ }
+
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_COUNT, labelNames, labelValues, arg.getCount()));
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_SUM,
+ labelNames,
+ labelValues,
+ arg.getCount() * arg.getMean()));
+ return null;
+ }
+ },
+ new Function<LastValueDataDouble, Void>() {
+ @Override
+ public Void apply(LastValueDataDouble arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getLastValue()));
+ return null;
+ }
+ },
+ new Function<LastValueDataLong, Void>() {
+ @Override
+ public Void apply(LastValueDataLong arg) {
+ samples.add(new Sample(name, labelNames, labelValues, arg.getLastValue()));
+ return null;
+ }
+ },
+ new Function<AggregationData, Void>() {
+ @Override
+ public Void apply(AggregationData arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof AggregationData.MeanData) {
+ AggregationData.MeanData meanData = (AggregationData.MeanData) arg;
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_COUNT, labelNames, labelValues, meanData.getCount()));
+ samples.add(
+ new MetricFamilySamples.Sample(
+ name + SAMPLE_SUFFIX_SUM,
+ labelNames,
+ labelValues,
+ meanData.getCount() * meanData.getMean()));
+ return null;
+ }
+ throw new IllegalArgumentException("Unknown Aggregation.");
+ }
+ });
+
+ return samples;
+ }
+
+ // Converts the list of tag keys to a list of string label names. Also sanitizes the tag keys.
+ @VisibleForTesting
+ static List<String> convertToLabelNames(List<TagKey> tagKeys) {
+ final List<String> labelNames = new ArrayList<String>(tagKeys.size());
+ for (TagKey tagKey : tagKeys) {
+ labelNames.add(Collector.sanitizeMetricName(tagKey.getName()));
+ }
+ return labelNames;
+ }
+
+ // Returns true if there is an "le" label name in histogram label names, returns false otherwise.
+ // Similar check to
+ // https://github.com/prometheus/client_java/commit/ed184d8e50c82e98bb2706723fff764424840c3a#diff-c505abbde72dd6bf36e89917b3469404R78
+ static boolean containsDisallowedLeLabelForHistogram(List<String> labelNames, Type type) {
+ if (!Type.HISTOGRAM.equals(type)) {
+ return false;
+ }
+ for (String label : labelNames) {
+ if (LABEL_NAME_BUCKET_BOUND.equals(label)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private PrometheusExportUtils() {}
+}
diff --git a/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java
new file mode 100644
index 00000000..d555c92b
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollector.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.containsDisallowedLeLabelForHistogram;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.convertToLabelNames;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.getType;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import io.opencensus.common.Scope;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import io.prometheus.client.Collector;
+import io.prometheus.client.CollectorRegistry;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * OpenCensus Stats {@link Collector} for Prometheus.
+ *
+ * @since 0.12
+ */
+@SuppressWarnings("deprecation")
+public final class PrometheusStatsCollector extends Collector implements Collector.Describable {
+
+ private static final Logger logger = Logger.getLogger(PrometheusStatsCollector.class.getName());
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+
+ private final ViewManager viewManager;
+
+ /**
+ * Creates a {@link PrometheusStatsCollector} and registers it to Prometheus {@link
+ * CollectorRegistry#defaultRegistry}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * PrometheusStatsCollector.createAndRegister(PrometheusStatsConfiguration.builder().build());
+ * }</pre>
+ *
+ * @throws IllegalArgumentException if a {@code PrometheusStatsCollector} has already been created
+ * and registered.
+ * @since 0.12
+ */
+ public static void createAndRegister() {
+ new PrometheusStatsCollector(Stats.getViewManager()).register();
+ }
+
+ /**
+ * Creates a {@link PrometheusStatsCollector} and registers it to the given Prometheus {@link
+ * CollectorRegistry} in the {@link PrometheusStatsConfiguration}.
+ *
+ * <p>If {@code CollectorRegistry} of the configuration is not set, the collector will use {@link
+ * CollectorRegistry#defaultRegistry}.
+ *
+ * @throws IllegalArgumentException if a {@code PrometheusStatsCollector} has already been created
+ * and registered.
+ * @since 0.13
+ */
+ public static void createAndRegister(PrometheusStatsConfiguration configuration) {
+ CollectorRegistry registry = configuration.getRegistry();
+ if (registry == null) {
+ registry = CollectorRegistry.defaultRegistry;
+ }
+ new PrometheusStatsCollector(Stats.getViewManager()).register(registry);
+ }
+
+ @Override
+ public List<MetricFamilySamples> collect() {
+ List<MetricFamilySamples> samples = Lists.newArrayList();
+ Span span =
+ tracer
+ .spanBuilder("ExportStatsToPrometheus")
+ .setSampler(probabilitySampler)
+ .setRecordEvents(true)
+ .startSpan();
+ span.addAnnotation("Collect Prometheus Metric Samples.");
+ Scope scope = tracer.withSpan(span);
+ try {
+ for (View view : viewManager.getAllExportedViews()) {
+ if (containsDisallowedLeLabelForHistogram(
+ convertToLabelNames(view.getColumns()),
+ getType(view.getAggregation(), view.getWindow()))) {
+ continue; // silently skip Distribution views with "le" tag key
+ }
+ try {
+ ViewData viewData = viewManager.getView(view.getName());
+ if (viewData == null) {
+ continue;
+ } else {
+ samples.add(PrometheusExportUtils.createMetricFamilySamples(viewData));
+ }
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown when collecting metric samples.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception thrown when collecting Prometheus Metric Samples: "
+ + exceptionMessage(e)));
+ }
+ }
+ span.addAnnotation("Finish collecting Prometheus Metric Samples.");
+ } finally {
+ scope.close();
+ span.end();
+ }
+ return samples;
+ }
+
+ @Override
+ public List<MetricFamilySamples> describe() {
+ List<MetricFamilySamples> samples = Lists.newArrayList();
+ Span span =
+ tracer
+ .spanBuilder("DescribeMetricsForPrometheus")
+ .setSampler(probabilitySampler)
+ .setRecordEvents(true)
+ .startSpan();
+ span.addAnnotation("Describe Prometheus Metrics.");
+ Scope scope = tracer.withSpan(span);
+ try {
+ for (View view : viewManager.getAllExportedViews()) {
+ try {
+ samples.add(PrometheusExportUtils.createDescribableMetricFamilySamples(view));
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown when describing metrics.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception thrown when describing Prometheus Metrics: " + exceptionMessage(e)));
+ }
+ }
+ span.addAnnotation("Finish describing Prometheus Metrics.");
+ } finally {
+ scope.close();
+ span.end();
+ }
+ return samples;
+ }
+
+ @VisibleForTesting
+ PrometheusStatsCollector(ViewManager viewManager) {
+ this.viewManager = viewManager;
+ Tracing.getExportComponent()
+ .getSampledSpanStore()
+ .registerSpanNamesForCollection(
+ ImmutableList.of("DescribeMetricsForPrometheus", "ExportStatsToPrometheus"));
+ }
+
+ private static String exceptionMessage(Throwable e) {
+ return e.getMessage() != null ? e.getMessage() : e.getClass().getName();
+ }
+}
diff --git a/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java
new file mode 100644
index 00000000..3e8b95ed
--- /dev/null
+++ b/exporters/stats/prometheus/src/main/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsConfiguration.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import com.google.auto.value.AutoValue;
+import io.prometheus.client.CollectorRegistry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link PrometheusStatsCollector}.
+ *
+ * @since 0.13
+ */
+@AutoValue
+@Immutable
+public abstract class PrometheusStatsConfiguration {
+
+ PrometheusStatsConfiguration() {}
+
+ /**
+ * Returns the Prometheus {@link CollectorRegistry}.
+ *
+ * @return the Prometheus {@code CollectorRegistry}.
+ * @since 0.13
+ */
+ @Nullable
+ public abstract CollectorRegistry getRegistry();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.13
+ */
+ public static Builder builder() {
+ return new AutoValue_PrometheusStatsConfiguration.Builder();
+ }
+
+ /**
+ * Builder for {@link PrometheusStatsConfiguration}.
+ *
+ * @since 0.13
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the given Prometheus {@link CollectorRegistry}.
+ *
+ * @param registry the Prometheus {@code CollectorRegistry}.
+ * @return this.
+ * @since 0.13
+ */
+ public abstract Builder setRegistry(CollectorRegistry registry);
+
+ /**
+ * Builds a new {@link PrometheusStatsConfiguration} with current settings.
+ *
+ * @return a {@code PrometheusStatsConfiguration}.
+ * @since 0.13
+ */
+ public abstract PrometheusStatsConfiguration build();
+ }
+}
diff --git a/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java
new file mode 100644
index 00000000..ca8315b9
--- /dev/null
+++ b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusExportUtilsTest.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.LABEL_NAME_BUCKET_BOUND;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_BUCKET;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_COUNT;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.SAMPLE_SUFFIX_SUM;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.convertToLabelNames;
+
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+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.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.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.Arrays;
+import java.util.Collections;
+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 PrometheusExportUtils}. */
+@RunWith(JUnit4.class)
+public class PrometheusExportUtilsTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final Interval INTERVAL = Interval.create(ONE_SECOND);
+ private static final Sum SUM = Sum.create();
+ private static final Count COUNT = Count.create();
+ private static final Mean MEAN = Mean.create();
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(-5.0, 0.0, 5.0));
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+ private static final LastValue LAST_VALUE = LastValue.create();
+ private static final View.Name VIEW_NAME_1 = View.Name.create("view1");
+ private static final View.Name VIEW_NAME_2 = View.Name.create("view2");
+ private static final View.Name VIEW_NAME_3 = View.Name.create("view-3");
+ private static final View.Name VIEW_NAME_4 = View.Name.create("-view4");
+ private static final String DESCRIPTION = "View description";
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure", "description", "1");
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = TagKey.create("k2");
+ private static final TagKey K3 = TagKey.create("k-3");
+ private static final TagKey TAG_KEY_LE = TagKey.create(LABEL_NAME_BUCKET_BOUND);
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+ private static final TagValue V3 = TagValue.create("v-3");
+ private static final SumDataDouble SUM_DATA_DOUBLE = SumDataDouble.create(-5.5);
+ private static final SumDataLong SUM_DATA_LONG = SumDataLong.create(123456789);
+ private static final CountData COUNT_DATA = CountData.create(12345);
+ private static final MeanData MEAN_DATA = MeanData.create(3.4, 22);
+ private static final DistributionData DISTRIBUTION_DATA =
+ DistributionData.create(4.4, 5, -3.2, 15.7, 135.22, Arrays.asList(0L, 2L, 2L, 1L));
+ private static final LastValueDataDouble LAST_VALUE_DATA_DOUBLE = LastValueDataDouble.create(7.9);
+ private static final LastValueDataLong LAST_VALUE_DATA_LONG = LastValueDataLong.create(66666666);
+ private static final View VIEW1 =
+ View.create(
+ VIEW_NAME_1, DESCRIPTION, MEASURE_DOUBLE, COUNT, Arrays.asList(K1, K2), CUMULATIVE);
+ private static final View VIEW2 =
+ View.create(VIEW_NAME_2, DESCRIPTION, MEASURE_DOUBLE, MEAN, Arrays.asList(K3), CUMULATIVE);
+ private static final View VIEW3 =
+ View.create(
+ VIEW_NAME_3, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(K1), CUMULATIVE);
+ private static final View VIEW4 =
+ View.create(VIEW_NAME_4, DESCRIPTION, MEASURE_DOUBLE, COUNT, Arrays.asList(K1), INTERVAL);
+ private static final View DISTRIBUTION_VIEW_WITH_LE_KEY =
+ View.create(
+ VIEW_NAME_1,
+ DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(K1, TAG_KEY_LE),
+ CUMULATIVE);
+ private static final CumulativeData CUMULATIVE_DATA =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ private static final IntervalData INTERVAL_DATA = IntervalData.create(Timestamp.fromMillis(1000));
+ private static final String SAMPLE_NAME = "view";
+
+ @Test
+ public void testConstants() {
+ assertThat(SAMPLE_SUFFIX_BUCKET).isEqualTo("_bucket");
+ assertThat(SAMPLE_SUFFIX_COUNT).isEqualTo("_count");
+ assertThat(SAMPLE_SUFFIX_SUM).isEqualTo("_sum");
+ assertThat(LABEL_NAME_BUCKET_BOUND).isEqualTo("le");
+ }
+
+ @Test
+ public void getType() {
+ assertThat(PrometheusExportUtils.getType(COUNT, INTERVAL)).isEqualTo(Type.UNTYPED);
+ assertThat(PrometheusExportUtils.getType(COUNT, CUMULATIVE)).isEqualTo(Type.COUNTER);
+ assertThat(PrometheusExportUtils.getType(DISTRIBUTION, CUMULATIVE)).isEqualTo(Type.HISTOGRAM);
+ assertThat(PrometheusExportUtils.getType(SUM, CUMULATIVE)).isEqualTo(Type.UNTYPED);
+ assertThat(PrometheusExportUtils.getType(MEAN, CUMULATIVE)).isEqualTo(Type.SUMMARY);
+ assertThat(PrometheusExportUtils.getType(LAST_VALUE, CUMULATIVE)).isEqualTo(Type.GAUGE);
+ }
+
+ @Test
+ public void createDescribableMetricFamilySamples() {
+ assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW1))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view1", Type.COUNTER, DESCRIPTION, Collections.<Sample>emptyList()));
+ assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW2))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view2", Type.SUMMARY, DESCRIPTION, Collections.<Sample>emptyList()));
+ assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW3))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view_3", Type.HISTOGRAM, DESCRIPTION, Collections.<Sample>emptyList()));
+ assertThat(PrometheusExportUtils.createDescribableMetricFamilySamples(VIEW4))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "_view4", Type.UNTYPED, DESCRIPTION, Collections.<Sample>emptyList()));
+ }
+
+ @Test
+ public void getSamples() {
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1, K2)),
+ Arrays.asList(V1, V2),
+ SUM_DATA_DOUBLE,
+ SUM))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), -5.5));
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K3)),
+ Arrays.asList(V3),
+ SUM_DATA_LONG,
+ SUM))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k_3"), Arrays.asList("v-3"), 123456789));
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1, K3)),
+ Arrays.asList(V1, null),
+ COUNT_DATA,
+ COUNT))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k1", "k_3"), Arrays.asList("v1", ""), 12345));
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K3)),
+ Arrays.asList(V3),
+ MEAN_DATA,
+ MEAN))
+ .containsExactly(
+ new Sample(SAMPLE_NAME + "_count", Arrays.asList("k_3"), Arrays.asList("v-3"), 22),
+ new Sample(SAMPLE_NAME + "_sum", Arrays.asList("k_3"), Arrays.asList("v-3"), 74.8))
+ .inOrder();
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1)),
+ Arrays.asList(V1),
+ DISTRIBUTION_DATA,
+ DISTRIBUTION))
+ .containsExactly(
+ new Sample(
+ SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "-5.0"), 0),
+ new Sample(
+ SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "0.0"), 2),
+ new Sample(
+ SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "5.0"), 4),
+ new Sample(
+ SAMPLE_NAME + "_bucket", Arrays.asList("k1", "le"), Arrays.asList("v1", "+Inf"), 5),
+ new Sample(SAMPLE_NAME + "_count", Arrays.asList("k1"), Arrays.asList("v1"), 5),
+ new Sample(SAMPLE_NAME + "_sum", Arrays.asList("k1"), Arrays.asList("v1"), 22.0))
+ .inOrder();
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1, K2)),
+ Arrays.asList(V1, V2),
+ LAST_VALUE_DATA_DOUBLE,
+ LAST_VALUE))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 7.9));
+ assertThat(
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K3)),
+ Arrays.asList(V3),
+ LAST_VALUE_DATA_LONG,
+ LAST_VALUE))
+ .containsExactly(
+ new Sample(SAMPLE_NAME, Arrays.asList("k_3"), Arrays.asList("v-3"), 66666666));
+ }
+
+ @Test
+ public void getSamples_KeysAndValuesHaveDifferentSizes() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Label names and tag values have different sizes.");
+ PrometheusExportUtils.getSamples(
+ SAMPLE_NAME,
+ convertToLabelNames(Arrays.asList(K1, K2, K3)),
+ Arrays.asList(V1, V2),
+ DISTRIBUTION_DATA,
+ DISTRIBUTION);
+ }
+
+ @Test
+ public void createDescribableMetricFamilySamples_Histogram_DisallowLeLabelName() {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage(
+ "Prometheus Histogram cannot have a label named 'le', "
+ + "because it is a reserved label for bucket boundaries. "
+ + "Please remove this tag key from your view.");
+ PrometheusExportUtils.createDescribableMetricFamilySamples(DISTRIBUTION_VIEW_WITH_LE_KEY);
+ }
+
+ @Test
+ public void createMetricFamilySamples() {
+ assertThat(
+ PrometheusExportUtils.createMetricFamilySamples(
+ ViewData.create(
+ VIEW1, ImmutableMap.of(Arrays.asList(V1, V2), COUNT_DATA), CUMULATIVE_DATA)))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view1",
+ Type.COUNTER,
+ DESCRIPTION,
+ Arrays.asList(
+ new Sample(
+ "view1", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 12345))));
+ assertThat(
+ PrometheusExportUtils.createMetricFamilySamples(
+ ViewData.create(
+ VIEW2, ImmutableMap.of(Arrays.asList(V1), MEAN_DATA), CUMULATIVE_DATA)))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view2",
+ Type.SUMMARY,
+ DESCRIPTION,
+ Arrays.asList(
+ new Sample("view2_count", Arrays.asList("k_3"), Arrays.asList("v1"), 22),
+ new Sample("view2_sum", Arrays.asList("k_3"), Arrays.asList("v1"), 74.8))));
+ assertThat(
+ PrometheusExportUtils.createMetricFamilySamples(
+ ViewData.create(
+ VIEW3, ImmutableMap.of(Arrays.asList(V3), DISTRIBUTION_DATA), CUMULATIVE_DATA)))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "view_3",
+ Type.HISTOGRAM,
+ DESCRIPTION,
+ Arrays.asList(
+ new Sample(
+ "view_3_bucket",
+ Arrays.asList("k1", "le"),
+ Arrays.asList("v-3", "-5.0"),
+ 0),
+ new Sample(
+ "view_3_bucket", Arrays.asList("k1", "le"), Arrays.asList("v-3", "0.0"), 2),
+ new Sample(
+ "view_3_bucket", Arrays.asList("k1", "le"), Arrays.asList("v-3", "5.0"), 4),
+ new Sample(
+ "view_3_bucket",
+ Arrays.asList("k1", "le"),
+ Arrays.asList("v-3", "+Inf"),
+ 5),
+ new Sample("view_3_count", Arrays.asList("k1"), Arrays.asList("v-3"), 5),
+ new Sample("view_3_sum", Arrays.asList("k1"), Arrays.asList("v-3"), 22.0))));
+ assertThat(
+ PrometheusExportUtils.createMetricFamilySamples(
+ ViewData.create(
+ VIEW4, ImmutableMap.of(Arrays.asList(V1), COUNT_DATA), INTERVAL_DATA)))
+ .isEqualTo(
+ new MetricFamilySamples(
+ "_view4",
+ Type.UNTYPED,
+ DESCRIPTION,
+ Arrays.asList(
+ new Sample("_view4", Arrays.asList("k1"), Arrays.asList("v1"), 12345))));
+ }
+}
diff --git a/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java
new file mode 100644
index 00000000..3bd98451
--- /dev/null
+++ b/exporters/stats/prometheus/src/test/java/io/opencensus/exporter/stats/prometheus/PrometheusStatsCollectorTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2018, 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.exporter.stats.prometheus;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.prometheus.PrometheusExportUtils.LABEL_NAME_BUCKET_BOUND;
+import static org.mockito.Mockito.doReturn;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Stats;
+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.TagKey;
+import io.opencensus.tags.TagValue;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.Collector.Type;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link PrometheusStatsCollector}. */
+@RunWith(JUnit4.class)
+public class PrometheusStatsCollectorTest {
+
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(-5.0, 0.0, 5.0));
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+ private static final View.Name VIEW_NAME = View.Name.create("view1");
+ private static final String DESCRIPTION = "View description";
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure", "description", "1");
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = TagKey.create("k2");
+ private static final TagKey LE_TAG_KEY = TagKey.create(LABEL_NAME_BUCKET_BOUND);
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+ private static final DistributionData DISTRIBUTION_DATA =
+ DistributionData.create(4.4, 5, -3.2, 15.7, 135.22, Arrays.asList(0L, 2L, 2L, 1L));
+ private static final View VIEW =
+ View.create(
+ VIEW_NAME, DESCRIPTION, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(K1, K2), CUMULATIVE);
+ private static final View VIEW_WITH_LE_TAG_KEY =
+ View.create(
+ VIEW_NAME,
+ DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(K1, LE_TAG_KEY),
+ CUMULATIVE);
+ private static final CumulativeData CUMULATIVE_DATA =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ private static final ViewData VIEW_DATA =
+ ViewData.create(
+ VIEW, ImmutableMap.of(Arrays.asList(V1, V2), DISTRIBUTION_DATA), CUMULATIVE_DATA);
+ private static final ViewData VIEW_DATA_WITH_LE_TAG_KEY =
+ ViewData.create(
+ VIEW_WITH_LE_TAG_KEY,
+ ImmutableMap.of(Arrays.asList(V1, V2), DISTRIBUTION_DATA),
+ CUMULATIVE_DATA);
+
+ @Mock private ViewManager mockViewManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ doReturn(ImmutableSet.of(VIEW)).when(mockViewManager).getAllExportedViews();
+ doReturn(VIEW_DATA).when(mockViewManager).getView(VIEW_NAME);
+ }
+
+ @Test
+ public void testCollect() {
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+ String name = "view1";
+ assertThat(collector.collect())
+ .containsExactly(
+ new MetricFamilySamples(
+ "view1",
+ Type.HISTOGRAM,
+ "View description",
+ Arrays.asList(
+ new Sample(
+ name + "_bucket",
+ Arrays.asList("k1", "k2", "le"),
+ Arrays.asList("v1", "v2", "-5.0"),
+ 0),
+ new Sample(
+ name + "_bucket",
+ Arrays.asList("k1", "k2", "le"),
+ Arrays.asList("v1", "v2", "0.0"),
+ 2),
+ new Sample(
+ name + "_bucket",
+ Arrays.asList("k1", "k2", "le"),
+ Arrays.asList("v1", "v2", "5.0"),
+ 4),
+ new Sample(
+ name + "_bucket",
+ Arrays.asList("k1", "k2", "le"),
+ Arrays.asList("v1", "v2", "+Inf"),
+ 5),
+ new Sample(
+ name + "_count", Arrays.asList("k1", "k2"), Arrays.asList("v1", "v2"), 5),
+ new Sample(
+ name + "_sum",
+ Arrays.asList("k1", "k2"),
+ Arrays.asList("v1", "v2"),
+ 22.0))));
+ }
+
+ @Test
+ public void testCollect_SkipDistributionViewWithLeTagKey() {
+ doReturn(ImmutableSet.of(VIEW_WITH_LE_TAG_KEY)).when(mockViewManager).getAllExportedViews();
+ doReturn(VIEW_DATA_WITH_LE_TAG_KEY).when(mockViewManager).getView(VIEW_NAME);
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+ assertThat(collector.collect()).isEmpty();
+ }
+
+ @Test
+ public void testDescribe() {
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(mockViewManager);
+ assertThat(collector.describe())
+ .containsExactly(
+ new MetricFamilySamples(
+ "view1", Type.HISTOGRAM, "View description", Collections.<Sample>emptyList()));
+ }
+
+ @Test
+ public void testCollect_WithNoopViewManager() {
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(Stats.getViewManager());
+ assertThat(collector.collect()).isEmpty();
+ }
+
+ @Test
+ public void testDescribe_WithNoopViewManager() {
+ PrometheusStatsCollector collector = new PrometheusStatsCollector(Stats.getViewManager());
+ assertThat(collector.describe()).isEmpty();
+ }
+}
diff --git a/exporters/stats/signalfx/README.md b/exporters/stats/signalfx/README.md
new file mode 100644
index 00000000..7c61f896
--- /dev/null
+++ b/exporters/stats/signalfx/README.md
@@ -0,0 +1,76 @@
+# OpenCensus SignalFx Stats Exporter
+
+The _OpenCensus SignalFx Stats Exporter_ is a stats exporter that
+exports data to [SignalFx](https://signalfx.com), a real-time monitoring
+solution for cloud and distributed applications. SignalFx ingests that
+data and offers various visualizations on charts, dashboards and service
+maps, as well as real-time anomaly detection.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have a [SignalFx](https://signalfx.com)
+account and corresponding [data ingest
+token](https://docs.signalfx.com/en/latest/admin-guide/tokens.html).
+
+#### Java versions
+
+This exporter requires Java 7 or above.
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-stats-signalfx</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+
+```
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-stats-signalfx:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) {
+ // SignalFx token is read from Java system properties.
+ // Stats will be reported every second by default.
+ SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().build());
+ }
+}
+```
+
+If you want to pass in the token yourself, or set a different reporting
+interval, use:
+
+```java
+// Use token "your_signalfx_token" and report every 5 seconds.
+SignalFxStatsExporter.create(
+ SignalFxStatsConfiguration.builder()
+ .setToken("your_signalfx_token")
+ .setExportInterval(Duration.create(5, 0))
+ .build());
+```
diff --git a/exporters/stats/signalfx/build.gradle b/exporters/stats/signalfx/build.gradle
new file mode 100644
index 00000000..d496b1e5
--- /dev/null
+++ b/exporters/stats/signalfx/build.gradle
@@ -0,0 +1,23 @@
+description = 'OpenCensus SignalFx Stats Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ compile (libraries.signalfx_java) {
+ // Prefer library version.
+ exclude group: 'com.google.guava', module: 'guava'
+ }
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java
new file mode 100644
index 00000000..5601a54c
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxMetricsSenderFactory.java
@@ -0,0 +1,59 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.signalfx.endpoint.SignalFxEndpoint;
+import com.signalfx.metrics.auth.StaticAuthToken;
+import com.signalfx.metrics.connection.HttpDataPointProtobufReceiverFactory;
+import com.signalfx.metrics.connection.HttpEventProtobufReceiverFactory;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import java.net.URI;
+import java.util.Collections;
+
+/** Interface for creators of {@link AggregateMetricSender}. */
+interface SignalFxMetricsSenderFactory {
+
+ /**
+ * Creates a new SignalFx metrics sender instance.
+ *
+ * @param endpoint The SignalFx ingest endpoint URL.
+ * @param token The SignalFx ingest token.
+ * @param errorHandler An {@link OnSendErrorHandler} through which errors when sending data to
+ * SignalFx will be communicated.
+ * @return The created {@link AggregateMetricSender} instance.
+ */
+ AggregateMetricSender create(URI endpoint, String token, OnSendErrorHandler errorHandler);
+
+ /** The default, concrete implementation of this interface. */
+ SignalFxMetricsSenderFactory DEFAULT =
+ new SignalFxMetricsSenderFactory() {
+ @Override
+ @SuppressWarnings("nullness")
+ public AggregateMetricSender create(
+ URI endpoint, String token, OnSendErrorHandler errorHandler) {
+ SignalFxEndpoint sfx =
+ new SignalFxEndpoint(endpoint.getScheme(), endpoint.getHost(), endpoint.getPort());
+ return new AggregateMetricSender(
+ null,
+ new HttpDataPointProtobufReceiverFactory(sfx).setVersion(2),
+ new HttpEventProtobufReceiverFactory(sfx),
+ new StaticAuthToken(token),
+ Collections.singleton(errorHandler));
+ }
+ };
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java
new file mode 100644
index 00000000..2eb75c4c
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptor.java
@@ -0,0 +1,188 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Function;
+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.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Adapter for a {@code ViewData}'s contents into SignalFx datapoints. */
+@SuppressWarnings("deprecation")
+final class SignalFxSessionAdaptor {
+
+ private SignalFxSessionAdaptor() {}
+
+ /**
+ * Converts the given view data into datapoints that can be sent to SignalFx.
+ *
+ * <p>The view name is used as the metric name, and the aggregation type and aggregation window
+ * type determine the metric type.
+ *
+ * @param data The {@link ViewData} containing the aggregation data of each combination of tag
+ * values.
+ * @return A list of datapoints for the corresponding metric timeseries of this view's metric.
+ */
+ static List<DataPoint> adapt(ViewData data) {
+ View view = data.getView();
+ List<TagKey> keys = view.getColumns();
+
+ MetricType metricType = getMetricTypeForAggregation(view.getAggregation(), view.getWindow());
+ if (metricType == null) {
+ return Collections.emptyList();
+ }
+
+ List<DataPoint> datapoints = new ArrayList<>(data.getAggregationMap().size());
+ for (Map.Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ data.getAggregationMap().entrySet()) {
+ datapoints.add(
+ DataPoint.newBuilder()
+ .setMetric(view.getName().asString())
+ .setMetricType(metricType)
+ .addAllDimensions(createDimensions(keys, entry.getKey()))
+ .setValue(createDatum(entry.getValue()))
+ .build());
+ }
+ return datapoints;
+ }
+
+ @VisibleForTesting
+ @javax.annotation.Nullable
+ static MetricType getMetricTypeForAggregation(
+ Aggregation aggregation, View.AggregationWindow window) {
+ if (aggregation instanceof Aggregation.Mean || aggregation instanceof Aggregation.LastValue) {
+ return MetricType.GAUGE;
+ } else if (aggregation instanceof Aggregation.Count || aggregation instanceof Aggregation.Sum) {
+ if (window instanceof View.AggregationWindow.Cumulative) {
+ return MetricType.CUMULATIVE_COUNTER;
+ }
+ // TODO(mpetazzoni): support incremental counters when AggregationWindow.Interval is ready
+ }
+
+ // TODO(mpetazzoni): add support for histograms (Aggregation.Distribution).
+ return null;
+ }
+
+ @VisibleForTesting
+ static Iterable<Dimension> createDimensions(
+ List<TagKey> keys, List</*@Nullable*/ TagValue> values) {
+ Preconditions.checkArgument(
+ keys.size() == values.size(), "TagKeys and TagValues don't have the same size.");
+ List<Dimension> dimensions = new ArrayList<>(keys.size());
+ for (ListIterator<TagKey> it = keys.listIterator(); it.hasNext(); ) {
+ TagKey key = it.next();
+ TagValue value = values.get(it.previousIndex());
+ if (value == null || Strings.isNullOrEmpty(value.asString())) {
+ continue;
+ }
+ dimensions.add(createDimension(key, value));
+ }
+ return dimensions;
+ }
+
+ @VisibleForTesting
+ static Dimension createDimension(TagKey key, TagValue value) {
+ return Dimension.newBuilder().setKey(key.getName()).setValue(value.asString()).build();
+ }
+
+ @VisibleForTesting
+ static Datum createDatum(AggregationData data) {
+ final Datum.Builder builder = Datum.newBuilder();
+ data.match(
+ new Function<SumDataDouble, Void>() {
+ @Override
+ public Void apply(SumDataDouble arg) {
+ builder.setDoubleValue(arg.getSum());
+ return null;
+ }
+ },
+ new Function<SumDataLong, Void>() {
+ @Override
+ public Void apply(SumDataLong arg) {
+ builder.setIntValue(arg.getSum());
+ return null;
+ }
+ },
+ new Function<CountData, Void>() {
+ @Override
+ public Void apply(CountData arg) {
+ builder.setIntValue(arg.getCount());
+ return null;
+ }
+ },
+ new Function<DistributionData, Void>() {
+ @Override
+ public Void apply(DistributionData arg) {
+ // TODO(mpetazzoni): add histogram support.
+ throw new IllegalArgumentException("Distribution aggregations are not supported");
+ }
+ },
+ new Function<LastValueDataDouble, Void>() {
+ @Override
+ public Void apply(LastValueDataDouble arg) {
+ builder.setDoubleValue(arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<LastValueDataLong, Void>() {
+ @Override
+ public Void apply(LastValueDataLong arg) {
+ builder.setIntValue(arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<AggregationData, Void>() {
+ @Override
+ public Void apply(AggregationData arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof AggregationData.MeanData) {
+ builder.setDoubleValue(((AggregationData.MeanData) arg).getMean());
+ return null;
+ }
+ throw new IllegalArgumentException("Unknown Aggregation.");
+ }
+ });
+ return builder.build();
+ }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java
new file mode 100644
index 00000000..e8b4d756
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfiguration.java
@@ -0,0 +1,153 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link SignalFxStatsExporter}.
+ *
+ * @since 0.11
+ */
+@AutoValue
+@Immutable
+public abstract class SignalFxStatsConfiguration {
+
+ /**
+ * The default SignalFx ingest API URL.
+ *
+ * @since 0.11
+ */
+ public static final URI DEFAULT_SIGNALFX_ENDPOINT;
+
+ static {
+ try {
+ DEFAULT_SIGNALFX_ENDPOINT = new URI("https://ingest.signalfx.com");
+ } catch (URISyntaxException e) {
+ // This shouldn't happen if DEFAULT_SIGNALFX_ENDPOINT was typed in correctly.
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * The default stats export interval.
+ *
+ * @since 0.11
+ */
+ public static final Duration DEFAULT_EXPORT_INTERVAL = Duration.create(1, 0);
+
+ private static final Duration ZERO = Duration.create(0, 0);
+
+ SignalFxStatsConfiguration() {}
+
+ /**
+ * Returns the SignalFx ingest API URL.
+ *
+ * @return the SignalFx ingest API URL.
+ * @since 0.11
+ */
+ public abstract URI getIngestEndpoint();
+
+ /**
+ * Returns the authentication token.
+ *
+ * @return the authentication token.
+ * @since 0.11
+ */
+ public abstract String getToken();
+
+ /**
+ * Returns the export interval between pushes to SignalFx.
+ *
+ * @return the export interval.
+ * @since 0.11
+ */
+ public abstract Duration getExportInterval();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.11
+ */
+ public static Builder builder() {
+ return new AutoValue_SignalFxStatsConfiguration.Builder()
+ .setIngestEndpoint(DEFAULT_SIGNALFX_ENDPOINT)
+ .setExportInterval(DEFAULT_EXPORT_INTERVAL);
+ }
+
+ /**
+ * Builder for {@link SignalFxStatsConfiguration}.
+ *
+ * @since 0.11
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the given SignalFx ingest API URL.
+ *
+ * @param url the SignalFx ingest API URL.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setIngestEndpoint(URI url);
+
+ /**
+ * Sets the given authentication token.
+ *
+ * @param token the authentication token.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setToken(String token);
+
+ /**
+ * Sets the export interval.
+ *
+ * @param exportInterval the export interval between pushes to SignalFx.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setExportInterval(Duration exportInterval);
+
+ abstract SignalFxStatsConfiguration autoBuild();
+
+ /**
+ * Builds a new {@link SignalFxStatsConfiguration} with current settings.
+ *
+ * @return a {@code SignalFxStatsConfiguration}.
+ * @since 0.11
+ */
+ public SignalFxStatsConfiguration build() {
+ SignalFxStatsConfiguration config = autoBuild();
+ Preconditions.checkArgument(
+ !Strings.isNullOrEmpty(config.getToken()), "Invalid SignalFx token");
+ Preconditions.checkArgument(
+ config.getExportInterval().compareTo(ZERO) > 0, "Interval duration must be positive");
+ return config;
+ }
+ }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java
new file mode 100644
index 00000000..f7915b71
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporter.java
@@ -0,0 +1,109 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.ViewManager;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Exporter to SignalFx.
+ *
+ * <p>Example of usage:
+ *
+ * <pre><code>
+ * public static void main(String[] args) {
+ * SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().build());
+ * ... // Do work.
+ * }
+ * </code></pre>
+ *
+ * @since 0.11
+ */
+public final class SignalFxStatsExporter {
+
+ private static final Object monitor = new Object();
+
+ private final SignalFxStatsConfiguration configuration;
+ private final SignalFxStatsExporterWorkerThread workerThread;
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static SignalFxStatsExporter exporter = null;
+
+ private SignalFxStatsExporter(SignalFxStatsConfiguration configuration, ViewManager viewManager) {
+ Preconditions.checkNotNull(configuration, "SignalFx stats exporter configuration");
+ this.configuration = configuration;
+ this.workerThread =
+ new SignalFxStatsExporterWorkerThread(
+ SignalFxMetricsSenderFactory.DEFAULT,
+ configuration.getIngestEndpoint(),
+ configuration.getToken(),
+ configuration.getExportInterval(),
+ viewManager);
+ }
+
+ /**
+ * Creates a SignalFx Stats exporter from the given {@link SignalFxStatsConfiguration}.
+ *
+ * <p>If {@code ingestEndpoint} is not set on the configuration, the exporter will use {@link
+ * SignalFxStatsConfiguration#DEFAULT_SIGNALFX_ENDPOINT}.
+ *
+ * <p>If {@code exportInterval} is not set on the configuration, the exporter will use {@link
+ * SignalFxStatsConfiguration#DEFAULT_EXPORT_INTERVAL}.
+ *
+ * @param configuration the {@code SignalFxStatsConfiguration}.
+ * @throws IllegalStateException if a SignalFx exporter is already created.
+ * @since 0.11
+ */
+ public static void create(SignalFxStatsConfiguration configuration) {
+ synchronized (monitor) {
+ Preconditions.checkState(exporter == null, "SignalFx stats exporter is already created.");
+ exporter = new SignalFxStatsExporter(configuration, Stats.getViewManager());
+ exporter.workerThread.start();
+ }
+ }
+
+ @VisibleForTesting
+ static void unsafeResetExporter() {
+ synchronized (monitor) {
+ if (exporter != null) {
+ SignalFxStatsExporterWorkerThread workerThread = exporter.workerThread;
+ if (workerThread != null && workerThread.isAlive()) {
+ try {
+ workerThread.interrupt();
+ workerThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ exporter = null;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ @Nullable
+ static SignalFxStatsConfiguration unsafeGetConfig() {
+ synchronized (monitor) {
+ return exporter != null ? exporter.configuration : null;
+ }
+ }
+}
diff --git a/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java
new file mode 100644
index 00000000..348778e2
--- /dev/null
+++ b/exporters/stats/signalfx/src/main/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThread.java
@@ -0,0 +1,105 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.signalfx.metrics.errorhandler.MetricError;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import com.signalfx.metrics.flush.AggregateMetricSender.Session;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Worker {@code Thread} that polls ViewData from the Stats's ViewManager and exports to SignalFx.
+ *
+ * <p>{@code SignalFxStatsExporterWorkerThread} is a daemon {@code Thread}
+ */
+final class SignalFxStatsExporterWorkerThread extends Thread {
+
+ private static final Logger logger =
+ Logger.getLogger(SignalFxStatsExporterWorkerThread.class.getName());
+
+ private static final OnSendErrorHandler ERROR_HANDLER =
+ new OnSendErrorHandler() {
+ @Override
+ public void handleError(MetricError error) {
+ logger.log(Level.WARNING, "Unable to send metrics to SignalFx: {0}", error.getMessage());
+ }
+ };
+
+ private final long intervalMs;
+ private final ViewManager views;
+ private final AggregateMetricSender sender;
+
+ SignalFxStatsExporterWorkerThread(
+ SignalFxMetricsSenderFactory factory,
+ URI endpoint,
+ String token,
+ Duration interval,
+ ViewManager views) {
+ this.intervalMs = interval.toMillis();
+ this.views = views;
+ this.sender = factory.create(endpoint, token, ERROR_HANDLER);
+
+ setDaemon(true);
+ setName(getClass().getSimpleName());
+ logger.log(Level.FINE, "Initialized SignalFx exporter to {0}.", endpoint);
+ }
+
+ @VisibleForTesting
+ void export() throws IOException {
+ Session session = sender.createSession();
+ try {
+ for (View view : views.getAllExportedViews()) {
+ ViewData data = views.getView(view.getName());
+ if (data == null) {
+ continue;
+ }
+
+ for (DataPoint datapoint : SignalFxSessionAdaptor.adapt(data)) {
+ session.setDatapoint(datapoint);
+ }
+ }
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ export();
+ Thread.sleep(intervalMs);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ break;
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown by the SignalFx stats exporter", e);
+ }
+ }
+ logger.log(Level.INFO, "SignalFx stats exporter stopped.");
+ }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java
new file mode 100644
index 00000000..34f4dfa7
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxSessionAdaptorTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Duration;
+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.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+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.View;
+import io.opencensus.stats.View.AggregationWindow;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SignalFxSessionAdaptorTest {
+
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Mock private View view;
+
+ @Mock private ViewData viewData;
+
+ @Before
+ public void setUp() {
+ Mockito.when(view.getName()).thenReturn(Name.create("view-name"));
+ Mockito.when(view.getColumns()).thenReturn(ImmutableList.of(TagKey.create("animal")));
+ Mockito.when(viewData.getView()).thenReturn(view);
+ }
+
+ @Test
+ public void checkMetricTypeFromAggregation() {
+ assertNull(SignalFxSessionAdaptor.getMetricTypeForAggregation(null, null));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ null, AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.GAUGE,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Mean.create(), AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.GAUGE,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Mean.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+ assertEquals(
+ MetricType.CUMULATIVE_COUNTER,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Count.create(), AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.CUMULATIVE_COUNTER,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Sum.create(), AggregationWindow.Cumulative.create()));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(Aggregation.Count.create(), null));
+ assertNull(SignalFxSessionAdaptor.getMetricTypeForAggregation(Aggregation.Sum.create(), null));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Count.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Sum.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+ assertNull(
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.Distribution.create(BucketBoundaries.create(ImmutableList.of(3.15d))),
+ AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.GAUGE,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.LastValue.create(), AggregationWindow.Cumulative.create()));
+ assertEquals(
+ MetricType.GAUGE,
+ SignalFxSessionAdaptor.getMetricTypeForAggregation(
+ Aggregation.LastValue.create(), AggregationWindow.Interval.create(ONE_SECOND)));
+ }
+
+ @Test
+ public void createDimensionsWithNonMatchingListSizes() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("don't have the same size");
+ SignalFxSessionAdaptor.createDimensions(
+ ImmutableList.of(TagKey.create("animal"), TagKey.create("color")),
+ ImmutableList.of(TagValue.create("dog")));
+ }
+
+ @Test
+ public void createDimensionsIgnoresEmptyValues() {
+ List<Dimension> dimensions =
+ Lists.newArrayList(
+ SignalFxSessionAdaptor.createDimensions(
+ ImmutableList.of(TagKey.create("animal"), TagKey.create("color")),
+ ImmutableList.of(TagValue.create("dog"), TagValue.create(""))));
+ assertEquals(1, dimensions.size());
+ assertEquals("animal", dimensions.get(0).getKey());
+ assertEquals("dog", dimensions.get(0).getValue());
+ }
+
+ @Test
+ public void createDimension() {
+ Dimension dimension =
+ SignalFxSessionAdaptor.createDimension(TagKey.create("animal"), TagValue.create("dog"));
+ assertEquals("animal", dimension.getKey());
+ assertEquals("dog", dimension.getValue());
+ }
+
+ @Test
+ public void unsupportedAggregationYieldsNoDatapoints() {
+ Mockito.when(view.getAggregation())
+ .thenReturn(
+ Aggregation.Distribution.create(BucketBoundaries.create(ImmutableList.of(3.15d))));
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+ List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+ assertEquals(0, datapoints.size());
+ }
+
+ @Test
+ public void noAggregationDataYieldsNoDatapoints() {
+ Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+ List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+ assertEquals(0, datapoints.size());
+ }
+
+ @Test
+ public void createDatumFromDoubleSum() {
+ SumDataDouble data = SumDataDouble.create(3.15d);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertTrue(datum.hasDoubleValue());
+ assertFalse(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(3.15d, datum.getDoubleValue(), 0d);
+ }
+
+ @Test
+ public void createDatumFromLongSum() {
+ SumDataLong data = SumDataLong.create(42L);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertFalse(datum.hasDoubleValue());
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(42L, datum.getIntValue());
+ }
+
+ @Test
+ public void createDatumFromCount() {
+ CountData data = CountData.create(42L);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertFalse(datum.hasDoubleValue());
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(42L, datum.getIntValue());
+ }
+
+ @Test
+ public void createDatumFromMean() {
+ MeanData data = MeanData.create(3.15d, 2L);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertTrue(datum.hasDoubleValue());
+ assertFalse(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(3.15d, datum.getDoubleValue(), 0d);
+ }
+
+ @Test
+ public void createDatumFromDistributionThrows() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Distribution aggregations are not supported");
+ SignalFxSessionAdaptor.createDatum(
+ DistributionData.create(5, 2, 0, 10, 40, ImmutableList.of(1L)));
+ }
+
+ @Test
+ public void createDatumFromLastValueDouble() {
+ LastValueDataDouble data = LastValueDataDouble.create(12.2);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertTrue(datum.hasDoubleValue());
+ assertFalse(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(12.2, datum.getDoubleValue(), 0d);
+ }
+
+ @Test
+ public void createDatumFromLastValueLong() {
+ LastValueDataLong data = LastValueDataLong.create(100000);
+ Datum datum = SignalFxSessionAdaptor.createDatum(data);
+ assertFalse(datum.hasDoubleValue());
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasStrValue());
+ assertEquals(100000, datum.getIntValue());
+ }
+
+ @Test
+ public void adaptViewIntoDatapoints() {
+ Map<List<TagValue>, AggregationData> map =
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ ImmutableList.of(TagValue.create("dog")),
+ SumDataLong.create(2L),
+ ImmutableList.of(TagValue.create("cat")),
+ SumDataLong.create(3L));
+ Mockito.when(viewData.getAggregationMap()).thenReturn(map);
+ Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+
+ List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+ assertEquals(2, datapoints.size());
+ for (DataPoint dp : datapoints) {
+ assertEquals("view-name", dp.getMetric());
+ assertEquals(MetricType.CUMULATIVE_COUNTER, dp.getMetricType());
+ assertEquals(1, dp.getDimensionsCount());
+ assertTrue(dp.hasValue());
+ assertFalse(dp.hasSource());
+
+ Datum datum = dp.getValue();
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasDoubleValue());
+ assertFalse(datum.hasStrValue());
+
+ Dimension dimension = dp.getDimensions(0);
+ assertEquals("animal", dimension.getKey());
+ switch (dimension.getValue()) {
+ case "dog":
+ assertEquals(2L, datum.getIntValue());
+ break;
+ case "cat":
+ assertEquals(3L, datum.getIntValue());
+ break;
+ default:
+ fail("unexpected dimension value");
+ }
+ }
+ }
+
+ @Test
+ public void adaptViewWithEmptyTagValueIntoDatapoints() {
+ Map<List<TagValue>, AggregationData> map =
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ ImmutableList.of(TagValue.create("dog")),
+ SumDataLong.create(2L),
+ ImmutableList.of(TagValue.create("")),
+ SumDataLong.create(3L));
+ Mockito.when(viewData.getAggregationMap()).thenReturn(map);
+ Mockito.when(view.getAggregation()).thenReturn(Aggregation.Count.create());
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+
+ List<DataPoint> datapoints = SignalFxSessionAdaptor.adapt(viewData);
+ assertEquals(2, datapoints.size());
+ for (DataPoint dp : datapoints) {
+ assertEquals("view-name", dp.getMetric());
+ assertEquals(MetricType.CUMULATIVE_COUNTER, dp.getMetricType());
+ assertTrue(dp.hasValue());
+ assertFalse(dp.hasSource());
+
+ Datum datum = dp.getValue();
+ assertTrue(datum.hasIntValue());
+ assertFalse(datum.hasDoubleValue());
+ assertFalse(datum.hasStrValue());
+
+ switch (dp.getDimensionsCount()) {
+ case 0:
+ assertEquals(3L, datum.getIntValue());
+ break;
+ case 1:
+ Dimension dimension = dp.getDimensions(0);
+ assertEquals("animal", dimension.getKey());
+ assertEquals("dog", dimension.getValue());
+ assertEquals(2L, datum.getIntValue());
+ break;
+ default:
+ fail("Unexpected number of dimensions on the created datapoint");
+ break;
+ }
+ }
+ }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.java
new file mode 100644
index 00000000..1d3508fb
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsConfigurationTest.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.exporter.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+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 SignalFxStatsConfiguration}. */
+@RunWith(JUnit4.class)
+public class SignalFxStatsConfigurationTest {
+
+ private static final String TEST_TOKEN = "token";
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void buildWithDefaults() {
+ SignalFxStatsConfiguration configuration =
+ SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+ assertEquals(TEST_TOKEN, configuration.getToken());
+ assertEquals(
+ SignalFxStatsConfiguration.DEFAULT_SIGNALFX_ENDPOINT, configuration.getIngestEndpoint());
+ assertEquals(
+ SignalFxStatsConfiguration.DEFAULT_EXPORT_INTERVAL, configuration.getExportInterval());
+ }
+
+ @Test
+ public void buildWithFields() throws URISyntaxException {
+ URI url = new URI("http://example.com");
+ Duration duration = Duration.create(5, 0);
+ SignalFxStatsConfiguration configuration =
+ SignalFxStatsConfiguration.builder()
+ .setToken(TEST_TOKEN)
+ .setIngestEndpoint(url)
+ .setExportInterval(duration)
+ .build();
+ assertEquals(TEST_TOKEN, configuration.getToken());
+ assertEquals(url, configuration.getIngestEndpoint());
+ assertEquals(duration, configuration.getExportInterval());
+ }
+
+ @Test
+ public void sameConfigurationsAreEqual() {
+ SignalFxStatsConfiguration config1 =
+ SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+ SignalFxStatsConfiguration config2 =
+ SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build();
+ assertEquals(config1, config2);
+ assertEquals(config1.hashCode(), config2.hashCode());
+ }
+
+ @Test
+ public void buildWithEmptyToken() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Invalid SignalFx token");
+ SignalFxStatsConfiguration.builder().setToken("").build();
+ }
+
+ @Test
+ public void buildWithNegativeDuration() {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Interval duration must be positive");
+ SignalFxStatsConfiguration.builder()
+ .setToken(TEST_TOKEN)
+ .setExportInterval(Duration.create(-1, 0))
+ .build();
+ }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java
new file mode 100644
index 00000000..cc5730b1
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import static org.junit.Assert.assertEquals;
+
+import io.opencensus.common.Duration;
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.junit.After;
+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 SignalFxStatsExporter}. */
+@RunWith(JUnit4.class)
+public class SignalFxStatsExporterTest {
+
+ private static final String TEST_TOKEN = "token";
+ private static final String TEST_ENDPOINT = "https://example.com";
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @After
+ public void tearDown() {
+ SignalFxStatsExporter.unsafeResetExporter();
+ }
+
+ @Test
+ public void createWithNullConfiguration() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("configuration");
+ SignalFxStatsExporter.create(null);
+ }
+
+ @Test
+ public void createWithNullHostUsesDefault() {
+ SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build());
+ assertEquals(
+ SignalFxStatsConfiguration.DEFAULT_SIGNALFX_ENDPOINT,
+ SignalFxStatsExporter.unsafeGetConfig().getIngestEndpoint());
+ }
+
+ @Test
+ public void createWithNullIntervalUsesDefault() {
+ SignalFxStatsExporter.create(SignalFxStatsConfiguration.builder().setToken(TEST_TOKEN).build());
+ assertEquals(
+ SignalFxStatsConfiguration.DEFAULT_EXPORT_INTERVAL,
+ SignalFxStatsExporter.unsafeGetConfig().getExportInterval());
+ }
+
+ @Test
+ public void createExporterTwice() {
+ SignalFxStatsConfiguration config =
+ SignalFxStatsConfiguration.builder()
+ .setToken(TEST_TOKEN)
+ .setExportInterval(ONE_SECOND)
+ .build();
+ SignalFxStatsExporter.create(config);
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("SignalFx stats exporter is already created.");
+ SignalFxStatsExporter.create(config);
+ }
+
+ @Test
+ public void createWithConfiguration() throws URISyntaxException {
+ SignalFxStatsConfiguration config =
+ SignalFxStatsConfiguration.builder()
+ .setToken(TEST_TOKEN)
+ .setIngestEndpoint(new URI(TEST_ENDPOINT))
+ .setExportInterval(ONE_SECOND)
+ .build();
+ SignalFxStatsExporter.create(config);
+ assertEquals(config, SignalFxStatsExporter.unsafeGetConfig());
+ }
+}
diff --git a/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java
new file mode 100644
index 00000000..d8852d5f
--- /dev/null
+++ b/exporters/stats/signalfx/src/test/java/io/opencensus/exporter/stats/signalfx/SignalFxStatsExporterWorkerThreadTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.exporter.stats.signalfx;
+
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.signalfx.metrics.errorhandler.OnSendErrorHandler;
+import com.signalfx.metrics.flush.AggregateMetricSender;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.DataPoint;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Datum;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.Dimension;
+import com.signalfx.metrics.protobuf.SignalFxProtocolBuffers.MetricType;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SignalFxStatsExporterWorkerThreadTest {
+
+ private static final String TEST_TOKEN = "token";
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+
+ @Mock private AggregateMetricSender.Session session;
+
+ @Mock private ViewManager viewManager;
+
+ @Mock private SignalFxMetricsSenderFactory factory;
+
+ private URI endpoint;
+
+ @Before
+ public void setUp() throws Exception {
+ endpoint = new URI("http://example.com");
+
+ Mockito.when(
+ factory.create(
+ Mockito.any(URI.class), Mockito.anyString(), Mockito.any(OnSendErrorHandler.class)))
+ .thenAnswer(
+ new Answer<AggregateMetricSender>() {
+ @Override
+ public AggregateMetricSender answer(InvocationOnMock invocation) {
+ Object[] args = invocation.getArguments();
+ AggregateMetricSender sender =
+ SignalFxMetricsSenderFactory.DEFAULT.create(
+ (URI) args[0], (String) args[1], (OnSendErrorHandler) args[2]);
+ AggregateMetricSender spy = Mockito.spy(sender);
+ Mockito.doReturn(session).when(spy).createSession();
+ return spy;
+ }
+ });
+ }
+
+ @Test
+ public void createThread() {
+ SignalFxStatsExporterWorkerThread thread =
+ new SignalFxStatsExporterWorkerThread(
+ factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+ assertTrue(thread.isDaemon());
+ assertThat(thread.getName(), startsWith("SignalFx"));
+ }
+
+ @Test
+ public void senderThreadInterruptStopsLoop() throws InterruptedException {
+ Mockito.when(session.setDatapoint(Mockito.any(DataPoint.class))).thenReturn(session);
+ Mockito.when(viewManager.getAllExportedViews()).thenReturn(ImmutableSet.<View>of());
+
+ SignalFxStatsExporterWorkerThread thread =
+ new SignalFxStatsExporterWorkerThread(
+ factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+ thread.start();
+ thread.interrupt();
+ thread.join(5000, 0);
+ assertFalse("Worker thread should have stopped", thread.isAlive());
+ }
+
+ @Test
+ public void setsDatapointsFromViewOnSession() throws IOException {
+ View view = Mockito.mock(View.class);
+ Name viewName = Name.create("test");
+ Mockito.when(view.getName()).thenReturn(viewName);
+ Mockito.when(view.getAggregation()).thenReturn(Aggregation.Mean.create());
+ Mockito.when(view.getWindow()).thenReturn(AggregationWindow.Cumulative.create());
+ Mockito.when(view.getColumns()).thenReturn(ImmutableList.of(TagKey.create("animal")));
+
+ ViewData viewData = Mockito.mock(ViewData.class);
+ Mockito.when(viewData.getView()).thenReturn(view);
+ Mockito.when(viewData.getAggregationMap())
+ .thenReturn(
+ ImmutableMap.<List<TagValue>, AggregationData>of(
+ ImmutableList.of(TagValue.create("cat")), MeanData.create(3.15d, 1)));
+
+ Mockito.when(viewManager.getAllExportedViews()).thenReturn(ImmutableSet.of(view));
+ Mockito.when(viewManager.getView(Mockito.eq(viewName))).thenReturn(viewData);
+
+ SignalFxStatsExporterWorkerThread thread =
+ new SignalFxStatsExporterWorkerThread(
+ factory, endpoint, TEST_TOKEN, ONE_SECOND, viewManager);
+ thread.export();
+
+ DataPoint datapoint =
+ DataPoint.newBuilder()
+ .setMetric("test")
+ .setMetricType(MetricType.GAUGE)
+ .addDimensions(Dimension.newBuilder().setKey("animal").setValue("cat").build())
+ .setValue(Datum.newBuilder().setDoubleValue(3.15d).build())
+ .build();
+ Mockito.verify(session).setDatapoint(Mockito.eq(datapoint));
+ Mockito.verify(session).close();
+ }
+}
diff --git a/exporters/stats/stackdriver/README.md b/exporters/stats/stackdriver/README.md
new file mode 100644
index 00000000..1b35c635
--- /dev/null
+++ b/exporters/stats/stackdriver/README.md
@@ -0,0 +1,171 @@
+# OpenCensus Stackdriver Stats Exporter
+
+The *OpenCensus Stackdriver Stats Exporter* is a stats exporter that exports data to
+Stackdriver Monitoring. [Stackdriver Monitoring][stackdriver-monitoring] provides visibility into
+the performance, uptime, and overall health of cloud-powered applications. Stackdriver ingests that
+data and generates insights via dashboards, charts, and alerts.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have an application that you'd like to monitor. The app can be on
+Google Cloud Platform, on-premise, or another cloud platform.
+
+In order to be able to push your stats to [Stackdriver Monitoring][stackdriver-monitoring], you must:
+
+1. [Create a Cloud project](https://support.google.com/cloud/answer/6251787?hl=en).
+2. [Enable billing](https://support.google.com/cloud/answer/6288653#new-billing).
+3. [Enable the Stackdriver Monitoring API](https://console.cloud.google.com/apis/dashboard).
+
+These steps enable the API but don't require that your app is hosted on Google Cloud Platform.
+
+### Hello "Stackdriver Stats"
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-stats-stackdriver</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-stats-stackdriver:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This uses the default configuration for authentication and a given project ID.
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) {
+ StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder().build());
+ }
+}
+```
+
+#### Set Monitored Resource for exporter
+
+By default, Stackdriver Stats Exporter will try to automatically detect the environment if your
+application is running on GCE, GKE or AWS EC2, and generate a corresponding Stackdriver GCE/GKE/EC2
+monitored resource. For GKE particularly, you may want to set up some environment variables so that
+Exporter can correctly identify your pod, cluster and container. Follow the Kubernetes instruction
+[here](https://cloud.google.com/kubernetes-engine/docs/tutorials/custom-metrics-autoscaling#exporting_metrics_from_the_application)
+and [here](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/).
+
+Otherwise, Exporter will use [a global Stackdriver monitored resource with a project_id label](https://cloud.google.com/monitoring/api/resources#tag_global),
+and it works fine when you have only one exporter running.
+
+If you want to have multiple processes exporting stats for the same metric concurrently, and your
+application is running on some different environment than GCE, GKE or AWS EC2 (for example DataFlow),
+please associate a unique monitored resource with each exporter if possible.
+Please note that there is also an "opencensus_task" metric label that uniquely identifies the
+uploaded stats.
+
+To set a custom MonitoredResource:
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) {
+ // A sample DataFlow monitored resource.
+ MonitoredResource myResource = MonitoredResource.newBuilder()
+ .setType("dataflow_job")
+ .putLabels("project_id", "my_project")
+ .putLabels("job_name", "my_job")
+ .putLabels("region", "us-east1")
+ .build();
+
+ // Set a custom MonitoredResource. Please make sure each Stackdriver Stats Exporter has a
+ // unique MonitoredResource.
+ StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder().setMonitoredResource(myResource).build());
+ }
+}
+```
+
+For a complete list of valid Stackdriver monitored resources, please refer to [Stackdriver
+Documentation](https://cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource).
+Please also note that although there are a lot of monitored resources available on [Stackdriver](https://cloud.google.com/monitoring/api/resources),
+only [a small subset of them](https://cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource)
+are compatible with the Opencensus Stackdriver Stats Exporter.
+
+#### Authentication
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the authentication see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication).
+
+If you prefer to manually set the credentials use:
+```
+StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder()
+ .setCredentials(new GoogleCredentials(new AccessToken(accessToken, expirationTime)))
+ .setProjectId("MyStackdriverProjectId")
+ .setExportInterval(Duration.create(10, 0))
+ .build());
+```
+
+#### Specifying a Project ID
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the project ID see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id).
+
+If you prefer to manually set the project ID use:
+```
+StackdriverStatsExporter.createAndRegister(
+ StackdriverStatsConfiguration.builder().setProjectId("MyStackdriverProjectId").build());
+```
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
+### Why did I get a PERMISSION_DENIED error from Stackdriver when using this exporter?
+To use our Stackdriver Stats exporter, you need to set up billing for your cloud project, since
+creating and uploading custom metrics to Stackdriver Monitoring is
+[not free](https://cloud.google.com/stackdriver/pricing_v2#monitoring-costs).
+
+To enable billing, follow the instructions [here](https://support.google.com/cloud/answer/6288653#new-billing).
+
+### What is "opencensus_task" metric label ?
+Stackdriver requires that each Timeseries to be updated only by one task at a time. A
+`Timeseries` is uniquely identified by the `MonitoredResource` and the `Metric`'s labels.
+Stackdriver exporter adds a new `Metric` label for each custom `Metric` to ensure the uniqueness
+of the `Timeseries`. The format of the label is: `{LANGUAGE}-{PID}@{HOSTNAME}`, if `{PID}` is not
+available a random number will be used.
+
+### Why did I get an error "java.lang.NoSuchMethodError: com.google.common...", like "java.lang.NoSuchMethodError:com.google.common.base.Throwables.throwIfInstanceOf"?
+This is probably because there is a version conflict on Guava in the dependency tree.
+
+For example, `com.google.common.base.Throwables.throwIfInstanceOf` is introduced to Guava 20.0.
+If your application has a dependency that bundles a Guava with version 19.0 or below
+(for example, gRPC 1.10.0), it might cause a `NoSuchMethodError` since
+`com.google.common.base.Throwables.throwIfInstanceOf` doesn't exist before Guava 20.0.
+
+In this case, please either add an explicit dependency on a newer version of Guava that has the
+new method (20.0 in the previous example), or if possible, upgrade the dependency that depends on
+Guava to a newer version that depends on the newer Guava (for example, upgrade to gRPC 1.12.0).
+
+[stackdriver-monitoring]: https://cloud.google.com/monitoring/
diff --git a/exporters/stats/stackdriver/build.gradle b/exporters/stats/stackdriver/build.gradle
new file mode 100644
index 00000000..0bc302a6
--- /dev/null
+++ b/exporters/stats/stackdriver/build.gradle
@@ -0,0 +1,30 @@
+description = 'OpenCensus Stats Stackdriver Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ project(':opencensus-contrib-monitored-resource-util'),
+ libraries.google_auth,
+ libraries.guava
+
+ compile (libraries.google_cloud_monitoring) {
+ // Prefer library version.
+ exclude group: 'com.google.guava', module: 'guava'
+
+ // Prefer library version.
+ exclude group: 'com.google.code.findbugs', module: 'jsr305'
+
+ // We will always be more up to date.
+ exclude group: 'io.opencensus', module: 'opencensus-api'
+ }
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+} \ No newline at end of file
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java
new file mode 100644
index 00000000..4f8715b0
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java
@@ -0,0 +1,518 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.api.Distribution;
+import com.google.api.Distribution.BucketOptions;
+import com.google.api.Distribution.BucketOptions.Explicit;
+import com.google.api.LabelDescriptor;
+import com.google.api.LabelDescriptor.ValueType;
+import com.google.api.Metric;
+import com.google.api.MetricDescriptor;
+import com.google.api.MetricDescriptor.MetricKind;
+import com.google.api.MonitoredResource;
+import com.google.cloud.MetadataConfig;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.monitoring.v3.Point;
+import com.google.monitoring.v3.TimeInterval;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.monitoring.v3.TypedValue;
+import com.google.monitoring.v3.TypedValue.Builder;
+import com.google.protobuf.Timestamp;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils;
+import io.opencensus.contrib.monitoredresource.util.ResourceType;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+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.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Util methods to convert OpenCensus Stats data models to StackDriver monitoring data models. */
+@SuppressWarnings("deprecation")
+final class StackdriverExportUtils {
+
+ // TODO(songya): do we want these constants to be customizable?
+ @VisibleForTesting static final String LABEL_DESCRIPTION = "OpenCensus TagKey";
+ @VisibleForTesting static final String OPENCENSUS_TASK = "opencensus_task";
+ @VisibleForTesting static final String OPENCENSUS_TASK_DESCRIPTION = "Opencensus task identifier";
+ private static final String GCP_GKE_CONTAINER = "k8s_container";
+ private static final String GCP_GCE_INSTANCE = "gce_instance";
+ private static final String AWS_EC2_INSTANCE = "aws_ec2_instance";
+ private static final String GLOBAL = "global";
+
+ private static final Logger logger = Logger.getLogger(StackdriverExportUtils.class.getName());
+ private static final String OPENCENSUS_TASK_VALUE_DEFAULT = generateDefaultTaskValue();
+ private static final String PROJECT_ID_LABEL_KEY = "project_id";
+
+ // Constant functions for ValueType.
+ private static final Function<Object, MetricDescriptor.ValueType> VALUE_TYPE_DOUBLE_FUNCTION =
+ Functions.returnConstant(MetricDescriptor.ValueType.DOUBLE);
+ private static final Function<Object, MetricDescriptor.ValueType> VALUE_TYPE_INT64_FUNCTION =
+ Functions.returnConstant(MetricDescriptor.ValueType.INT64);
+ private static final Function<Object, MetricDescriptor.ValueType>
+ VALUE_TYPE_UNRECOGNIZED_FUNCTION =
+ Functions.returnConstant(MetricDescriptor.ValueType.UNRECOGNIZED);
+ private static final Function<Object, MetricDescriptor.ValueType>
+ VALUE_TYPE_DISTRIBUTION_FUNCTION =
+ Functions.returnConstant(MetricDescriptor.ValueType.DISTRIBUTION);
+ private static final Function<Aggregation, MetricDescriptor.ValueType> valueTypeMeanFunction =
+ new Function<Aggregation, MetricDescriptor.ValueType>() {
+ @Override
+ public MetricDescriptor.ValueType apply(Aggregation arg) {
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof Aggregation.Mean) {
+ return MetricDescriptor.ValueType.DOUBLE;
+ }
+ return MetricDescriptor.ValueType.UNRECOGNIZED;
+ }
+ };
+
+ // Constant functions for MetricKind.
+ private static final Function<Object, MetricKind> METRIC_KIND_CUMULATIVE_FUNCTION =
+ Functions.returnConstant(MetricKind.CUMULATIVE);
+ private static final Function<Object, MetricKind> METRIC_KIND_UNRECOGNIZED_FUNCTION =
+ Functions.returnConstant(MetricKind.UNRECOGNIZED);
+
+ // Constant functions for TypedValue.
+ private static final Function<SumDataDouble, TypedValue> typedValueSumDoubleFunction =
+ new Function<SumDataDouble, TypedValue>() {
+ @Override
+ public TypedValue apply(SumDataDouble arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setDoubleValue(arg.getSum());
+ return builder.build();
+ }
+ };
+ private static final Function<SumDataLong, TypedValue> typedValueSumLongFunction =
+ new Function<SumDataLong, TypedValue>() {
+ @Override
+ public TypedValue apply(SumDataLong arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setInt64Value(arg.getSum());
+ return builder.build();
+ }
+ };
+ private static final Function<CountData, TypedValue> typedValueCountFunction =
+ new Function<CountData, TypedValue>() {
+ @Override
+ public TypedValue apply(CountData arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setInt64Value(arg.getCount());
+ return builder.build();
+ }
+ };
+ private static final Function<LastValueDataDouble, TypedValue> typedValueLastValueDoubleFunction =
+ new Function<LastValueDataDouble, TypedValue>() {
+ @Override
+ public TypedValue apply(LastValueDataDouble arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setDoubleValue(arg.getLastValue());
+ return builder.build();
+ }
+ };
+ private static final Function<LastValueDataLong, TypedValue> typedValueLastValueLongFunction =
+ new Function<LastValueDataLong, TypedValue>() {
+ @Override
+ public TypedValue apply(LastValueDataLong arg) {
+ Builder builder = TypedValue.newBuilder();
+ builder.setInt64Value(arg.getLastValue());
+ return builder.build();
+ }
+ };
+ private static final Function<AggregationData, TypedValue> typedValueMeanFunction =
+ new Function<AggregationData, TypedValue>() {
+ @Override
+ public TypedValue apply(AggregationData arg) {
+ Builder builder = TypedValue.newBuilder();
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ if (arg instanceof AggregationData.MeanData) {
+ builder.setDoubleValue(((AggregationData.MeanData) arg).getMean());
+ return builder.build();
+ }
+ throw new IllegalArgumentException("Unknown Aggregation");
+ }
+ };
+
+ private static String generateDefaultTaskValue() {
+ // Something like '<pid>@<hostname>', at least in Oracle and OpenJdk JVMs
+ final String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+ // If not the expected format then generate a random number.
+ if (jvmName.indexOf('@') < 1) {
+ String hostname = "localhost";
+ try {
+ hostname = InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ logger.log(Level.INFO, "Unable to get the hostname.", e);
+ }
+ // Generate a random number and use the same format "random_number@hostname".
+ return "java-" + new SecureRandom().nextInt() + "@" + hostname;
+ }
+ return "java-" + jvmName;
+ }
+
+ // Construct a MetricDescriptor using a View.
+ @javax.annotation.Nullable
+ static MetricDescriptor createMetricDescriptor(
+ View view, String projectId, String domain, String displayNamePrefix) {
+ if (!(view.getWindow() instanceof View.AggregationWindow.Cumulative)) {
+ // TODO(songya): Only Cumulative view will be exported to Stackdriver in this version.
+ return null;
+ }
+
+ MetricDescriptor.Builder builder = MetricDescriptor.newBuilder();
+ String viewName = view.getName().asString();
+ String type = generateType(viewName, domain);
+ // Name format refers to
+ // cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.metricDescriptors/create
+ builder.setName(String.format("projects/%s/metricDescriptors/%s", projectId, type));
+ builder.setType(type);
+ builder.setDescription(view.getDescription());
+ String displayName = createDisplayName(viewName, displayNamePrefix);
+ builder.setDisplayName(displayName);
+ for (TagKey tagKey : view.getColumns()) {
+ builder.addLabels(createLabelDescriptor(tagKey));
+ }
+ builder.addLabels(
+ LabelDescriptor.newBuilder()
+ .setKey(OPENCENSUS_TASK)
+ .setDescription(OPENCENSUS_TASK_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build());
+ builder.setUnit(createUnit(view.getAggregation(), view.getMeasure()));
+ builder.setMetricKind(createMetricKind(view.getWindow(), view.getAggregation()));
+ builder.setValueType(createValueType(view.getAggregation(), view.getMeasure()));
+ return builder.build();
+ }
+
+ private static String generateType(String viewName, String domain) {
+ return domain + viewName;
+ }
+
+ private static String createDisplayName(String viewName, String displayNamePrefix) {
+ return displayNamePrefix + viewName;
+ }
+
+ // Construct a LabelDescriptor from a TagKey
+ @VisibleForTesting
+ static LabelDescriptor createLabelDescriptor(TagKey tagKey) {
+ LabelDescriptor.Builder builder = LabelDescriptor.newBuilder();
+ builder.setKey(tagKey.getName());
+ builder.setDescription(LABEL_DESCRIPTION);
+ // Now we only support String tags
+ builder.setValueType(ValueType.STRING);
+ return builder.build();
+ }
+
+ // Construct a MetricKind from an AggregationWindow
+ @VisibleForTesting
+ static MetricKind createMetricKind(View.AggregationWindow window, Aggregation aggregation) {
+ if (aggregation instanceof LastValue) {
+ return MetricKind.GAUGE;
+ }
+ return window.match(
+ METRIC_KIND_CUMULATIVE_FUNCTION, // Cumulative
+ // TODO(songya): We don't support exporting Interval stats to StackDriver in this version.
+ METRIC_KIND_UNRECOGNIZED_FUNCTION, // Interval
+ METRIC_KIND_UNRECOGNIZED_FUNCTION);
+ }
+
+ // Construct a MetricDescriptor.ValueType from an Aggregation and a Measure
+ @VisibleForTesting
+ static String createUnit(Aggregation aggregation, final Measure measure) {
+ if (aggregation instanceof Aggregation.Count) {
+ return "1";
+ }
+ return measure.getUnit();
+ }
+
+ // Construct a MetricDescriptor.ValueType from an Aggregation and a Measure
+ @VisibleForTesting
+ static MetricDescriptor.ValueType createValueType(
+ Aggregation aggregation, final Measure measure) {
+ return aggregation.match(
+ Functions.returnConstant(
+ measure.match(
+ VALUE_TYPE_DOUBLE_FUNCTION, // Sum Double
+ VALUE_TYPE_INT64_FUNCTION, // Sum Long
+ VALUE_TYPE_UNRECOGNIZED_FUNCTION)),
+ VALUE_TYPE_INT64_FUNCTION, // Count
+ VALUE_TYPE_DISTRIBUTION_FUNCTION, // Distribution
+ Functions.returnConstant(
+ measure.match(
+ VALUE_TYPE_DOUBLE_FUNCTION, // LastValue Double
+ VALUE_TYPE_INT64_FUNCTION, // LastValue Long
+ VALUE_TYPE_UNRECOGNIZED_FUNCTION)),
+ valueTypeMeanFunction);
+ }
+
+ // Convert ViewData to a list of TimeSeries, so that ViewData can be uploaded to Stackdriver.
+ static List<TimeSeries> createTimeSeriesList(
+ @javax.annotation.Nullable ViewData viewData,
+ MonitoredResource monitoredResource,
+ String domain) {
+ List<TimeSeries> timeSeriesList = Lists.newArrayList();
+ if (viewData == null) {
+ return timeSeriesList;
+ }
+ View view = viewData.getView();
+ if (!(view.getWindow() instanceof View.AggregationWindow.Cumulative)) {
+ // TODO(songya): Only Cumulative view will be exported to Stackdriver in this version.
+ return timeSeriesList;
+ }
+
+ // Shared fields for all TimeSeries generated from the same ViewData
+ TimeSeries.Builder shared = TimeSeries.newBuilder();
+ shared.setMetricKind(createMetricKind(view.getWindow(), view.getAggregation()));
+ shared.setResource(monitoredResource);
+ shared.setValueType(createValueType(view.getAggregation(), view.getMeasure()));
+
+ // Each entry in AggregationMap will be converted into an independent TimeSeries object
+ for (Entry<List</*@Nullable*/ TagValue>, AggregationData> entry :
+ viewData.getAggregationMap().entrySet()) {
+ TimeSeries.Builder builder = shared.clone();
+ builder.setMetric(createMetric(view, entry.getKey(), domain));
+ builder.addPoints(
+ createPoint(entry.getValue(), viewData.getWindowData(), view.getAggregation()));
+ timeSeriesList.add(builder.build());
+ }
+
+ return timeSeriesList;
+ }
+
+ // Create a Metric using the TagKeys and TagValues.
+ @VisibleForTesting
+ static Metric createMetric(View view, List</*@Nullable*/ TagValue> tagValues, String domain) {
+ Metric.Builder builder = Metric.newBuilder();
+ // TODO(songya): use pre-defined metrics for canonical views
+ builder.setType(generateType(view.getName().asString(), domain));
+ Map<String, String> stringTagMap = Maps.newHashMap();
+ List<TagKey> columns = view.getColumns();
+ checkArgument(
+ tagValues.size() == columns.size(), "TagKeys and TagValues don't have same size.");
+ for (int i = 0; i < tagValues.size(); i++) {
+ TagKey key = columns.get(i);
+ TagValue value = tagValues.get(i);
+ if (value == null) {
+ continue;
+ }
+ stringTagMap.put(key.getName(), value.asString());
+ }
+ stringTagMap.put(OPENCENSUS_TASK, OPENCENSUS_TASK_VALUE_DEFAULT);
+ builder.putAllLabels(stringTagMap);
+ return builder.build();
+ }
+
+ // Create Point from AggregationData, AggregationWindowData and Aggregation.
+ @VisibleForTesting
+ static Point createPoint(
+ AggregationData aggregationData,
+ ViewData.AggregationWindowData windowData,
+ Aggregation aggregation) {
+ Point.Builder builder = Point.newBuilder();
+ builder.setInterval(createTimeInterval(windowData, aggregation));
+ builder.setValue(createTypedValue(aggregation, aggregationData));
+ return builder.build();
+ }
+
+ // Convert AggregationWindowData to TimeInterval, currently only support CumulativeData.
+ @VisibleForTesting
+ static TimeInterval createTimeInterval(
+ ViewData.AggregationWindowData windowData, final Aggregation aggregation) {
+ return windowData.match(
+ new Function<ViewData.AggregationWindowData.CumulativeData, TimeInterval>() {
+ @Override
+ public TimeInterval apply(ViewData.AggregationWindowData.CumulativeData arg) {
+ TimeInterval.Builder builder = TimeInterval.newBuilder();
+ builder.setEndTime(convertTimestamp(arg.getEnd()));
+ if (!(aggregation instanceof LastValue)) {
+ builder.setStartTime(convertTimestamp(arg.getStart()));
+ }
+ return builder.build();
+ }
+ },
+ Functions.<TimeInterval>throwIllegalArgumentException(),
+ Functions.<TimeInterval>throwIllegalArgumentException());
+ }
+
+ // Create a TypedValue using AggregationData and Aggregation
+ // Note TypedValue is "A single strongly-typed value", i.e only one field should be set.
+ @VisibleForTesting
+ static TypedValue createTypedValue(
+ final Aggregation aggregation, AggregationData aggregationData) {
+ return aggregationData.match(
+ typedValueSumDoubleFunction,
+ typedValueSumLongFunction,
+ typedValueCountFunction,
+ new Function<DistributionData, TypedValue>() {
+ @Override
+ public TypedValue apply(DistributionData arg) {
+ TypedValue.Builder builder = TypedValue.newBuilder();
+ checkArgument(
+ aggregation instanceof Aggregation.Distribution,
+ "Aggregation and AggregationData mismatch.");
+ builder.setDistributionValue(
+ createDistribution(
+ arg, ((Aggregation.Distribution) aggregation).getBucketBoundaries()));
+ return builder.build();
+ }
+ },
+ typedValueLastValueDoubleFunction,
+ typedValueLastValueLongFunction,
+ typedValueMeanFunction);
+ }
+
+ // Create a StackDriver Distribution from DistributionData and BucketBoundaries
+ @VisibleForTesting
+ static Distribution createDistribution(
+ DistributionData distributionData, BucketBoundaries bucketBoundaries) {
+ return Distribution.newBuilder()
+ .setBucketOptions(createBucketOptions(bucketBoundaries))
+ .addAllBucketCounts(distributionData.getBucketCounts())
+ .setCount(distributionData.getCount())
+ .setMean(distributionData.getMean())
+ // TODO(songya): uncomment this once Stackdriver supports setting max and min.
+ // .setRange(
+ // Range.newBuilder()
+ // .setMax(distributionData.getMax())
+ // .setMin(distributionData.getMin())
+ // .build())
+ .setSumOfSquaredDeviation(distributionData.getSumOfSquaredDeviations())
+ .build();
+ }
+
+ // Create BucketOptions from BucketBoundaries
+ @VisibleForTesting
+ static BucketOptions createBucketOptions(BucketBoundaries bucketBoundaries) {
+ return BucketOptions.newBuilder()
+ .setExplicitBuckets(Explicit.newBuilder().addAllBounds(bucketBoundaries.getBoundaries()))
+ .build();
+ }
+
+ // Convert a Census Timestamp to a StackDriver Timestamp
+ @VisibleForTesting
+ static Timestamp convertTimestamp(io.opencensus.common.Timestamp censusTimestamp) {
+ if (censusTimestamp.getSeconds() < 0) {
+ // Stackdriver doesn't handle negative timestamps.
+ return Timestamp.newBuilder().build();
+ }
+ return Timestamp.newBuilder()
+ .setSeconds(censusTimestamp.getSeconds())
+ .setNanos(censusTimestamp.getNanos())
+ .build();
+ }
+
+ /* Return a self-configured Stackdriver monitored resource. */
+ static MonitoredResource getDefaultResource() {
+ MonitoredResource.Builder builder = MonitoredResource.newBuilder();
+ io.opencensus.contrib.monitoredresource.util.MonitoredResource autoDetectedResource =
+ MonitoredResourceUtils.getDefaultResource();
+ if (autoDetectedResource == null) {
+ builder.setType(GLOBAL);
+ if (MetadataConfig.getProjectId() != null) {
+ // For default global resource, always use the project id from MetadataConfig. This allows
+ // stats from other projects (e.g from GAE running in another project) to be collected.
+ builder.putLabels(PROJECT_ID_LABEL_KEY, MetadataConfig.getProjectId());
+ }
+ return builder.build();
+ }
+ builder.setType(mapToStackdriverResourceType(autoDetectedResource.getResourceType()));
+ setMonitoredResourceLabelsForBuilder(builder, autoDetectedResource);
+ return builder.build();
+ }
+
+ private static String mapToStackdriverResourceType(ResourceType resourceType) {
+ switch (resourceType) {
+ case GCP_GCE_INSTANCE:
+ return GCP_GCE_INSTANCE;
+ case GCP_GKE_CONTAINER:
+ return GCP_GKE_CONTAINER;
+ case AWS_EC2_INSTANCE:
+ return AWS_EC2_INSTANCE;
+ }
+ throw new IllegalArgumentException("Unknown resource type.");
+ }
+
+ private static void setMonitoredResourceLabelsForBuilder(
+ MonitoredResource.Builder builder,
+ io.opencensus.contrib.monitoredresource.util.MonitoredResource autoDetectedResource) {
+ switch (autoDetectedResource.getResourceType()) {
+ case GCP_GCE_INSTANCE:
+ GcpGceInstanceMonitoredResource gcpGceInstanceMonitoredResource =
+ (GcpGceInstanceMonitoredResource) autoDetectedResource;
+ builder.putLabels(PROJECT_ID_LABEL_KEY, gcpGceInstanceMonitoredResource.getAccount());
+ builder.putLabels("instance_id", gcpGceInstanceMonitoredResource.getInstanceId());
+ builder.putLabels("zone", gcpGceInstanceMonitoredResource.getZone());
+ return;
+ case GCP_GKE_CONTAINER:
+ GcpGkeContainerMonitoredResource gcpGkeContainerMonitoredResource =
+ (GcpGkeContainerMonitoredResource) autoDetectedResource;
+ builder.putLabels(PROJECT_ID_LABEL_KEY, gcpGkeContainerMonitoredResource.getAccount());
+ builder.putLabels("cluster_name", gcpGkeContainerMonitoredResource.getClusterName());
+ builder.putLabels("container_name", gcpGkeContainerMonitoredResource.getContainerName());
+ builder.putLabels("namespace_name", gcpGkeContainerMonitoredResource.getNamespaceId());
+ builder.putLabels("pod_name", gcpGkeContainerMonitoredResource.getPodId());
+ builder.putLabels("location", gcpGkeContainerMonitoredResource.getZone());
+ return;
+ case AWS_EC2_INSTANCE:
+ AwsEc2InstanceMonitoredResource awsEc2InstanceMonitoredResource =
+ (AwsEc2InstanceMonitoredResource) autoDetectedResource;
+ builder.putLabels("aws_account", awsEc2InstanceMonitoredResource.getAccount());
+ builder.putLabels("instance_id", awsEc2InstanceMonitoredResource.getInstanceId());
+ builder.putLabels("region", "aws:" + awsEc2InstanceMonitoredResource.getRegion());
+ return;
+ }
+ throw new IllegalArgumentException("Unknown subclass of MonitoredResource.");
+ }
+
+ private StackdriverExportUtils() {}
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java
new file mode 100644
index 00000000..5ffed9d5
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorker.java
@@ -0,0 +1,274 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import com.google.api.MetricDescriptor;
+import com.google.api.MonitoredResource;
+import com.google.api.gax.rpc.ApiException;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.monitoring.v3.CreateMetricDescriptorRequest;
+import com.google.monitoring.v3.CreateTimeSeriesRequest;
+import com.google.monitoring.v3.ProjectName;
+import com.google.monitoring.v3.TimeSeries;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Scope;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/**
+ * Worker {@code Runnable} that polls ViewData from Stats library and batch export to StackDriver.
+ *
+ * <p>{@code StackdriverExporterWorker} will be started in a daemon {@code Thread}.
+ *
+ * <p>The state of this class should only be accessed from the thread which {@link
+ * StackdriverExporterWorker} resides in.
+ */
+@NotThreadSafe
+final class StackdriverExporterWorker implements Runnable {
+
+ private static final Logger logger = Logger.getLogger(StackdriverExporterWorker.class.getName());
+
+ // Stackdriver Monitoring v3 only accepts up to 200 TimeSeries per CreateTimeSeries call.
+ @VisibleForTesting static final int MAX_BATCH_EXPORT_SIZE = 200;
+
+ @VisibleForTesting static final String DEFAULT_DISPLAY_NAME_PREFIX = "OpenCensus/";
+ @VisibleForTesting static final String CUSTOM_METRIC_DOMAIN = "custom.googleapis.com/";
+
+ @VisibleForTesting
+ static final String CUSTOM_OPENCENSUS_DOMAIN = CUSTOM_METRIC_DOMAIN + "opencensus/";
+
+ private final long scheduleDelayMillis;
+ private final String projectId;
+ private final ProjectName projectName;
+ private final MetricServiceClient metricServiceClient;
+ private final ViewManager viewManager;
+ private final MonitoredResource monitoredResource;
+ private final String domain;
+ private final String displayNamePrefix;
+ private final Map<View.Name, View> registeredViews = new HashMap<View.Name, View>();
+
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+
+ StackdriverExporterWorker(
+ String projectId,
+ MetricServiceClient metricServiceClient,
+ Duration exportInterval,
+ ViewManager viewManager,
+ MonitoredResource monitoredResource,
+ @javax.annotation.Nullable String metricNamePrefix) {
+ this.scheduleDelayMillis = exportInterval.toMillis();
+ this.projectId = projectId;
+ projectName = ProjectName.newBuilder().setProject(projectId).build();
+ this.metricServiceClient = metricServiceClient;
+ this.viewManager = viewManager;
+ this.monitoredResource = monitoredResource;
+ this.domain = getDomain(metricNamePrefix);
+ this.displayNamePrefix = getDisplayNamePrefix(metricNamePrefix);
+
+ Tracing.getExportComponent()
+ .getSampledSpanStore()
+ .registerSpanNamesForCollection(
+ Collections.singletonList("ExportStatsToStackdriverMonitoring"));
+ }
+
+ // Returns true if the given view is successfully registered to Stackdriver Monitoring, or the
+ // exact same view has already been registered. Returns false otherwise.
+ @VisibleForTesting
+ boolean registerView(View view) {
+ View existing = registeredViews.get(view.getName());
+ if (existing != null) {
+ if (existing.equals(view)) {
+ // Ignore views that are already registered.
+ return true;
+ } else {
+ // If we upload a view that has the same name with a registered view but with different
+ // attributes, Stackdriver client will throw an exception.
+ logger.log(
+ Level.WARNING,
+ "A different view with the same name is already registered: " + existing);
+ return false;
+ }
+ }
+ registeredViews.put(view.getName(), view);
+
+ Span span = tracer.getCurrentSpan();
+ span.addAnnotation("Create Stackdriver Metric.");
+ // TODO(songya): don't need to create MetricDescriptor for RpcViewConstants once we defined
+ // canonical metrics. Registration is required only for custom view definitions. Canonical
+ // views should be pre-registered.
+ MetricDescriptor metricDescriptor =
+ StackdriverExportUtils.createMetricDescriptor(view, projectId, domain, displayNamePrefix);
+ if (metricDescriptor == null) {
+ // Don't register interval views in this version.
+ return false;
+ }
+
+ CreateMetricDescriptorRequest request =
+ CreateMetricDescriptorRequest.newBuilder()
+ .setName(projectName.toString())
+ .setMetricDescriptor(metricDescriptor)
+ .build();
+ try {
+ metricServiceClient.createMetricDescriptor(request);
+ span.addAnnotation("Finish creating MetricDescriptor.");
+ return true;
+ } catch (ApiException e) {
+ logger.log(Level.WARNING, "ApiException thrown when creating MetricDescriptor.", e);
+ span.setStatus(
+ Status.CanonicalCode.valueOf(e.getStatusCode().getCode().name())
+ .toStatus()
+ .withDescription(
+ "ApiException thrown when creating MetricDescriptor: " + exceptionMessage(e)));
+ return false;
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown when creating MetricDescriptor.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception thrown when creating MetricDescriptor: " + exceptionMessage(e)));
+ return false;
+ }
+ }
+
+ // Polls ViewData from Stats library for all exported views, and upload them as TimeSeries to
+ // StackDriver.
+ @VisibleForTesting
+ void export() {
+ List</*@Nullable*/ ViewData> viewDataList = Lists.newArrayList();
+ for (View view : viewManager.getAllExportedViews()) {
+ if (registerView(view)) {
+ // Only upload stats for valid views.
+ viewDataList.add(viewManager.getView(view.getName()));
+ }
+ }
+
+ List<TimeSeries> timeSeriesList = Lists.newArrayList();
+ for (/*@Nullable*/ ViewData viewData : viewDataList) {
+ timeSeriesList.addAll(
+ StackdriverExportUtils.createTimeSeriesList(viewData, monitoredResource, domain));
+ }
+ for (List<TimeSeries> batchedTimeSeries :
+ Lists.partition(timeSeriesList, MAX_BATCH_EXPORT_SIZE)) {
+ Span span = tracer.getCurrentSpan();
+ span.addAnnotation("Export Stackdriver TimeSeries.");
+ try {
+ CreateTimeSeriesRequest request =
+ CreateTimeSeriesRequest.newBuilder()
+ .setName(projectName.toString())
+ .addAllTimeSeries(batchedTimeSeries)
+ .build();
+ metricServiceClient.createTimeSeries(request);
+ span.addAnnotation("Finish exporting TimeSeries.");
+ } catch (ApiException e) {
+ logger.log(Level.WARNING, "ApiException thrown when exporting TimeSeries.", e);
+ span.setStatus(
+ Status.CanonicalCode.valueOf(e.getStatusCode().getCode().name())
+ .toStatus()
+ .withDescription(
+ "ApiException thrown when exporting TimeSeries: " + exceptionMessage(e)));
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown when exporting TimeSeries.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception thrown when exporting TimeSeries: " + exceptionMessage(e)));
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ Span span =
+ tracer
+ .spanBuilder("ExportStatsToStackdriverMonitoring")
+ .setRecordEvents(true)
+ .setSampler(probabilitySampler)
+ .startSpan();
+ Scope scope = tracer.withSpan(span);
+ try {
+ export();
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown by the Stackdriver stats exporter.", e);
+ span.setStatus(
+ Status.UNKNOWN.withDescription(
+ "Exception from Stackdriver Exporter: " + exceptionMessage(e)));
+ } finally {
+ scope.close();
+ span.end();
+ }
+ try {
+ Thread.sleep(scheduleDelayMillis);
+ } catch (InterruptedException ie) {
+ // Preserve the interruption status as per guidance and stop doing any work.
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+
+ private static String exceptionMessage(Throwable e) {
+ return e.getMessage() != null ? e.getMessage() : e.getClass().getName();
+ }
+
+ @VisibleForTesting
+ static String getDomain(@javax.annotation.Nullable String metricNamePrefix) {
+ String domain;
+ if (Strings.isNullOrEmpty(metricNamePrefix)) {
+ domain = CUSTOM_OPENCENSUS_DOMAIN;
+ } else {
+ if (!metricNamePrefix.endsWith("/")) {
+ domain = metricNamePrefix + '/';
+ } else {
+ domain = metricNamePrefix;
+ }
+ }
+ return domain;
+ }
+
+ @VisibleForTesting
+ static String getDisplayNamePrefix(@javax.annotation.Nullable String metricNamePrefix) {
+ if (metricNamePrefix == null) {
+ return DEFAULT_DISPLAY_NAME_PREFIX;
+ } else {
+ if (!metricNamePrefix.endsWith("/") && !metricNamePrefix.isEmpty()) {
+ metricNamePrefix += '/';
+ }
+ return metricNamePrefix;
+ }
+ }
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java
new file mode 100644
index 00000000..c4008ca1
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfiguration.java
@@ -0,0 +1,159 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import com.google.api.MonitoredResource;
+import com.google.auth.Credentials;
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link StackdriverStatsExporter}.
+ *
+ * @since 0.11
+ */
+@AutoValue
+@Immutable
+public abstract class StackdriverStatsConfiguration {
+
+ StackdriverStatsConfiguration() {}
+
+ /**
+ * Returns the {@link Credentials}.
+ *
+ * @return the {@code Credentials}.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract Credentials getCredentials();
+
+ /**
+ * Returns the project id.
+ *
+ * @return the project id.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract String getProjectId();
+
+ /**
+ * Returns the export interval between pushes to StackDriver.
+ *
+ * @return the export interval.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract Duration getExportInterval();
+
+ /**
+ * Returns the Stackdriver {@link MonitoredResource}.
+ *
+ * @return the {@code MonitoredResource}.
+ * @since 0.11
+ */
+ @Nullable
+ public abstract MonitoredResource getMonitoredResource();
+
+ /**
+ * Returns the name prefix for Stackdriver metrics.
+ *
+ * @return the metric name prefix.
+ * @since 0.16
+ */
+ @Nullable
+ public abstract String getMetricNamePrefix();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.11
+ */
+ public static Builder builder() {
+ return new AutoValue_StackdriverStatsConfiguration.Builder();
+ }
+
+ /**
+ * Builder for {@link StackdriverStatsConfiguration}.
+ *
+ * @since 0.11
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the given {@link Credentials}.
+ *
+ * @param credentials the {@code Credentials}.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setCredentials(Credentials credentials);
+
+ /**
+ * Sets the given project id.
+ *
+ * @param projectId the cloud project id.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setProjectId(String projectId);
+
+ /**
+ * Sets the export interval.
+ *
+ * @param exportInterval the export interval between pushes to StackDriver.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setExportInterval(Duration exportInterval);
+
+ /**
+ * Sets the {@link MonitoredResource}.
+ *
+ * @param monitoredResource the Stackdriver {@code MonitoredResource}.
+ * @return this.
+ * @since 0.11
+ */
+ public abstract Builder setMonitoredResource(MonitoredResource monitoredResource);
+
+ /**
+ * Sets the the name prefix for Stackdriver metrics.
+ *
+ * <p>It is suggested to use prefix with custom or external domain name, for example
+ * "custom.googleapis.com/myorg/" or "external.googleapis.com/prometheus/". If the given prefix
+ * doesn't start with a valid domain, we will add "custom.googleapis.com/" before the prefix.
+ *
+ * @param prefix the metric name prefix.
+ * @return this.
+ * @since 0.16
+ */
+ public abstract Builder setMetricNamePrefix(String prefix);
+
+ /**
+ * Builds a new {@link StackdriverStatsConfiguration} with current settings.
+ *
+ * @return a {@code StackdriverStatsConfiguration}.
+ * @since 0.11
+ */
+ public abstract StackdriverStatsConfiguration build();
+ }
+}
diff --git a/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java
new file mode 100644
index 00000000..51c54916
--- /dev/null
+++ b/exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporter.java
@@ -0,0 +1,363 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.api.MonitoredResource;
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.cloud.monitoring.v3.MetricServiceSettings;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.MoreExecutors;
+import io.opencensus.common.Duration;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.ViewManager;
+import java.io.IOException;
+import java.util.concurrent.ThreadFactory;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Exporter to Stackdriver Monitoring Client API v3.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre><code>
+ * public static void main(String[] args) {
+ * StackdriverStatsExporter.createAndRegister(
+ * StackdriverStatsConfiguration
+ * .builder()
+ * .setProjectId("MyStackdriverProjectId")
+ * .setExportInterval(Duration.fromMillis(100000))
+ * .build());
+ * ... // Do work.
+ * }
+ * </code></pre>
+ *
+ * @since 0.9
+ */
+public final class StackdriverStatsExporter {
+
+ private static final Object monitor = new Object();
+
+ private final Thread workerThread;
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static StackdriverStatsExporter exporter = null;
+
+ private static final Duration ZERO = Duration.create(0, 0);
+
+ @VisibleForTesting static final Duration DEFAULT_INTERVAL = Duration.create(60, 0);
+
+ private static final MonitoredResource DEFAULT_RESOURCE =
+ StackdriverExportUtils.getDefaultResource();
+
+ @VisibleForTesting
+ StackdriverStatsExporter(
+ String projectId,
+ MetricServiceClient metricServiceClient,
+ Duration exportInterval,
+ ViewManager viewManager,
+ MonitoredResource monitoredResource,
+ @Nullable String metricNamePrefix) {
+ checkArgument(exportInterval.compareTo(ZERO) > 0, "Duration must be positive");
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ projectId,
+ metricServiceClient,
+ exportInterval,
+ viewManager,
+ monitoredResource,
+ metricNamePrefix);
+ this.workerThread = new DaemonThreadFactory().newThread(worker);
+ }
+
+ /**
+ * Creates a StackdriverStatsExporter for an explicit project ID and using explicit credentials,
+ * with default Monitored Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * @param credentials a credentials used to authenticate API calls.
+ * @param projectId the cloud project id.
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @throws IllegalStateException if a Stackdriver exporter already exists.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.9
+ */
+ @Deprecated
+ public static void createAndRegisterWithCredentialsAndProjectId(
+ Credentials credentials, String projectId, Duration exportInterval) throws IOException {
+ checkNotNull(credentials, "credentials");
+ checkNotNull(projectId, "projectId");
+ checkNotNull(exportInterval, "exportInterval");
+ createInternal(credentials, projectId, exportInterval, null, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter for an explicit project ID, with default Monitored
+ * Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>This uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverStatsExporter.createWithCredentialsAndProjectId(
+ * GoogleCredentials.getApplicationDefault(), projectId);
+ * }</pre>
+ *
+ * @param projectId the cloud project id.
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.9
+ */
+ @Deprecated
+ public static void createAndRegisterWithProjectId(String projectId, Duration exportInterval)
+ throws IOException {
+ checkNotNull(projectId, "projectId");
+ checkNotNull(exportInterval, "exportInterval");
+ createInternal(null, projectId, exportInterval, null, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with a {@link StackdriverStatsConfiguration}.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>If {@code credentials} of the configuration is not set, the exporter will use the default
+ * application credentials. See {@link GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>If {@code projectId} of the configuration is not set, the exporter will use the default
+ * project ID configured. See {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * <p>If {@code exportInterval} of the configuration is not set, the exporter will use the default
+ * interval of one minute.
+ *
+ * <p>If {@code monitoredResources} of the configuration is not set, the exporter will try to
+ * create an appropriate {@code monitoredResources} based on the environment variables. In
+ * addition, please refer to
+ * cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource for a list of valid
+ * {@code MonitoredResource}s.
+ *
+ * <p>If {@code metricNamePrefix} of the configuration is not set, the exporter will use the
+ * default prefix "OpenCensus".
+ *
+ * @param configuration the {@code StackdriverStatsConfiguration}.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @since 0.11.0
+ */
+ public static void createAndRegister(StackdriverStatsConfiguration configuration)
+ throws IOException {
+ checkNotNull(configuration, "configuration");
+ createInternal(
+ configuration.getCredentials(),
+ configuration.getProjectId(),
+ configuration.getExportInterval(),
+ configuration.getMonitoredResource(),
+ configuration.getMetricNamePrefix());
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with default settings.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverStatsExporter.createAndRegister(StackdriverStatsConfiguration.builder().build());
+ * }</pre>
+ *
+ * <p>This method uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This method uses the default project ID configured. See {@link
+ * ServiceOptions#getDefaultProjectId}.
+ *
+ * <p>This method uses the default interval of one minute.
+ *
+ * <p>This method uses the default resource created from the environment variables.
+ *
+ * <p>This method uses the default display name prefix "OpenCensus".
+ *
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @since 0.11.0
+ */
+ public static void createAndRegister() throws IOException {
+ createInternal(null, null, null, null, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with default Monitored Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>This uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverStatsExporter.createWithProjectId(ServiceOptions.getDefaultProjectId());
+ * }</pre>
+ *
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.9
+ */
+ @Deprecated
+ public static void createAndRegister(Duration exportInterval) throws IOException {
+ checkNotNull(exportInterval, "exportInterval");
+ createInternal(null, null, exportInterval, null, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with an explicit project ID and a custom Monitored
+ * Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>Please refer to cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource
+ * for a list of valid {@code MonitoredResource}s.
+ *
+ * <p>This uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * @param projectId the cloud project id.
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @param monitoredResource the Monitored Resource used by exporter.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.10
+ */
+ @Deprecated
+ public static void createAndRegisterWithProjectIdAndMonitoredResource(
+ String projectId, Duration exportInterval, MonitoredResource monitoredResource)
+ throws IOException {
+ checkNotNull(projectId, "projectId");
+ checkNotNull(exportInterval, "exportInterval");
+ checkNotNull(monitoredResource, "monitoredResource");
+ createInternal(null, projectId, exportInterval, monitoredResource, null);
+ }
+
+ /**
+ * Creates a Stackdriver Stats exporter with a custom Monitored Resource.
+ *
+ * <p>Only one Stackdriver exporter can be created.
+ *
+ * <p>Please refer to cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource
+ * for a list of valid {@code MonitoredResource}s.
+ *
+ * <p>This uses the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * @param exportInterval the interval between pushing stats to StackDriver.
+ * @param monitoredResource the Monitored Resource used by exporter.
+ * @throws IllegalStateException if a Stackdriver exporter is already created.
+ * @deprecated in favor of {@link #createAndRegister(StackdriverStatsConfiguration)}.
+ * @since 0.10
+ */
+ @Deprecated
+ public static void createAndRegisterWithMonitoredResource(
+ Duration exportInterval, MonitoredResource monitoredResource) throws IOException {
+ checkNotNull(exportInterval, "exportInterval");
+ checkNotNull(monitoredResource, "monitoredResource");
+ createInternal(null, null, exportInterval, monitoredResource, null);
+ }
+
+ // Use createInternal() (instead of constructor) to enforce singleton.
+ private static void createInternal(
+ @Nullable Credentials credentials,
+ @Nullable String projectId,
+ @Nullable Duration exportInterval,
+ @Nullable MonitoredResource monitoredResource,
+ @Nullable String metricNamePrefix)
+ throws IOException {
+ projectId = projectId == null ? ServiceOptions.getDefaultProjectId() : projectId;
+ exportInterval = exportInterval == null ? DEFAULT_INTERVAL : exportInterval;
+ monitoredResource = monitoredResource == null ? DEFAULT_RESOURCE : monitoredResource;
+ synchronized (monitor) {
+ checkState(exporter == null, "Stackdriver stats exporter is already created.");
+ MetricServiceClient metricServiceClient;
+ // Initialize MetricServiceClient inside lock to avoid creating multiple clients.
+ if (credentials == null) {
+ metricServiceClient = MetricServiceClient.create();
+ } else {
+ metricServiceClient =
+ MetricServiceClient.create(
+ MetricServiceSettings.newBuilder()
+ .setCredentialsProvider(FixedCredentialsProvider.create(credentials))
+ .build());
+ }
+ exporter =
+ new StackdriverStatsExporter(
+ projectId,
+ metricServiceClient,
+ exportInterval,
+ Stats.getViewManager(),
+ monitoredResource,
+ metricNamePrefix);
+ exporter.workerThread.start();
+ }
+ }
+
+ // Resets exporter to null. Used only for unit tests.
+ @VisibleForTesting
+ static void unsafeResetExporter() {
+ synchronized (monitor) {
+ StackdriverStatsExporter.exporter = null;
+ }
+ }
+
+ /** A lightweight {@link ThreadFactory} to spawn threads in a GAE-Java7-compatible way. */
+ // TODO(Hailong): Remove this once we use a callback to implement the exporter.
+ static final class DaemonThreadFactory implements ThreadFactory {
+ // AppEngine runtimes have constraints on threading and socket handling
+ // that need to be accommodated.
+ public static final boolean IS_RESTRICTED_APPENGINE =
+ System.getProperty("com.google.appengine.runtime.environment") != null
+ && "1.7".equals(System.getProperty("java.specification.version"));
+ private static final ThreadFactory threadFactory = MoreExecutors.platformThreadFactory();
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread thread = threadFactory.newThread(r);
+ if (!IS_RESTRICTED_APPENGINE) {
+ thread.setName("ExportWorkerThread");
+ thread.setDaemon(true);
+ }
+ return thread;
+ }
+ }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java
new file mode 100644
index 00000000..cd536e8f
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java
@@ -0,0 +1,568 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX;
+
+import com.google.api.Distribution.BucketOptions;
+import com.google.api.Distribution.BucketOptions.Explicit;
+import com.google.api.LabelDescriptor;
+import com.google.api.LabelDescriptor.ValueType;
+import com.google.api.Metric;
+import com.google.api.MetricDescriptor;
+import com.google.api.MetricDescriptor.MetricKind;
+import com.google.api.MonitoredResource;
+import com.google.common.collect.ImmutableMap;
+import com.google.monitoring.v3.Point;
+import com.google.monitoring.v3.TimeInterval;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.monitoring.v3.TypedValue;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+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.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.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.lang.management.ManagementFactory;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+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 StackdriverExportUtils}. */
+@RunWith(JUnit4.class)
+public class StackdriverExportUtilsTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final TagKey KEY = TagKey.create("KEY");
+ private static final TagKey KEY_2 = TagKey.create("KEY2");
+ private static final TagKey KEY_3 = TagKey.create("KEY3");
+ private static final TagValue VALUE_1 = TagValue.create("VALUE1");
+ private static final TagValue VALUE_2 = TagValue.create("VALUE2");
+ private static final String MEASURE_UNIT = "us";
+ private static final String MEASURE_DESCRIPTION = "measure description";
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure1", MEASURE_DESCRIPTION, MEASURE_UNIT);
+ private static final MeasureLong MEASURE_LONG =
+ MeasureLong.create("measure2", MEASURE_DESCRIPTION, MEASURE_UNIT);
+ private static final String VIEW_NAME = "view";
+ private static final String VIEW_DESCRIPTION = "view description";
+ private static final Duration TEN_SECONDS = Duration.create(10, 0);
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(0.0, 1.0, 3.0, 5.0));
+ private static final Sum SUM = Sum.create();
+ private static final Count COUNT = Count.create();
+ private static final Mean MEAN = Mean.create();
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+ private static final LastValue LAST_VALUE = LastValue.create();
+ private static final String PROJECT_ID = "id";
+ private static final MonitoredResource DEFAULT_RESOURCE =
+ MonitoredResource.newBuilder().setType("global").build();
+ private static final String DEFAULT_TASK_VALUE =
+ "java-" + ManagementFactory.getRuntimeMXBean().getName();
+
+ @Test
+ public void testConstant() {
+ assertThat(StackdriverExportUtils.LABEL_DESCRIPTION).isEqualTo("OpenCensus TagKey");
+ }
+
+ @Test
+ public void createLabelDescriptor() {
+ assertThat(StackdriverExportUtils.createLabelDescriptor(TagKey.create("string")))
+ .isEqualTo(
+ LabelDescriptor.newBuilder()
+ .setKey("string")
+ .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build());
+ }
+
+ @Test
+ public void createMetricKind() {
+ assertThat(StackdriverExportUtils.createMetricKind(CUMULATIVE, SUM))
+ .isEqualTo(MetricKind.CUMULATIVE);
+ assertThat(StackdriverExportUtils.createMetricKind(INTERVAL, COUNT))
+ .isEqualTo(MetricKind.UNRECOGNIZED);
+ assertThat(StackdriverExportUtils.createMetricKind(CUMULATIVE, LAST_VALUE))
+ .isEqualTo(MetricKind.GAUGE);
+ assertThat(StackdriverExportUtils.createMetricKind(INTERVAL, LAST_VALUE))
+ .isEqualTo(MetricKind.GAUGE);
+ }
+
+ @Test
+ public void createValueType() {
+ assertThat(StackdriverExportUtils.createValueType(SUM, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+ assertThat(StackdriverExportUtils.createValueType(SUM, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.INT64);
+ assertThat(StackdriverExportUtils.createValueType(COUNT, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.INT64);
+ assertThat(StackdriverExportUtils.createValueType(COUNT, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.INT64);
+ assertThat(StackdriverExportUtils.createValueType(MEAN, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+ assertThat(StackdriverExportUtils.createValueType(MEAN, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+ assertThat(StackdriverExportUtils.createValueType(DISTRIBUTION, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+ assertThat(StackdriverExportUtils.createValueType(DISTRIBUTION, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+ assertThat(StackdriverExportUtils.createValueType(LAST_VALUE, MEASURE_DOUBLE))
+ .isEqualTo(MetricDescriptor.ValueType.DOUBLE);
+ assertThat(StackdriverExportUtils.createValueType(LAST_VALUE, MEASURE_LONG))
+ .isEqualTo(MetricDescriptor.ValueType.INT64);
+ }
+
+ @Test
+ public void createUnit() {
+ assertThat(StackdriverExportUtils.createUnit(SUM, MEASURE_DOUBLE)).isEqualTo(MEASURE_UNIT);
+ assertThat(StackdriverExportUtils.createUnit(COUNT, MEASURE_DOUBLE)).isEqualTo("1");
+ assertThat(StackdriverExportUtils.createUnit(MEAN, MEASURE_DOUBLE)).isEqualTo(MEASURE_UNIT);
+ assertThat(StackdriverExportUtils.createUnit(DISTRIBUTION, MEASURE_DOUBLE))
+ .isEqualTo(MEASURE_UNIT);
+ assertThat(StackdriverExportUtils.createUnit(LAST_VALUE, MEASURE_DOUBLE))
+ .isEqualTo(MEASURE_UNIT);
+ }
+
+ @Test
+ public void createMetric() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ assertThat(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+ .isEqualTo(
+ Metric.newBuilder()
+ .setType("custom.googleapis.com/opencensus/" + VIEW_NAME)
+ .putLabels("KEY", "VALUE1")
+ .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+ .build());
+ }
+
+ @Test
+ public void createMetric_WithExternalMetricDomain() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ String prometheusDomain = "external.googleapis.com/prometheus/";
+ assertThat(StackdriverExportUtils.createMetric(view, Arrays.asList(VALUE_1), prometheusDomain))
+ .isEqualTo(
+ Metric.newBuilder()
+ .setType(prometheusDomain + VIEW_NAME)
+ .putLabels("KEY", "VALUE1")
+ .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+ .build());
+ }
+
+ @Test
+ public void createMetric_skipNullTagValue() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY, KEY_2, KEY_3),
+ CUMULATIVE);
+ assertThat(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_1, null, VALUE_2), CUSTOM_OPENCENSUS_DOMAIN))
+ .isEqualTo(
+ Metric.newBuilder()
+ .setType("custom.googleapis.com/opencensus/" + VIEW_NAME)
+ .putLabels("KEY", "VALUE1")
+ .putLabels("KEY3", "VALUE2")
+ .putLabels(StackdriverExportUtils.OPENCENSUS_TASK, DEFAULT_TASK_VALUE)
+ .build());
+ }
+
+ @Test
+ public void createMetric_throwWhenTagKeysAndValuesHaveDifferentSize() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY, KEY_2, KEY_3),
+ CUMULATIVE);
+ List<TagValue> tagValues = Arrays.asList(VALUE_1, null);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("TagKeys and TagValues don't have same size.");
+ StackdriverExportUtils.createMetric(view, tagValues, CUSTOM_OPENCENSUS_DOMAIN);
+ }
+
+ @Test
+ public void convertTimestamp() {
+ Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+ assertThat(StackdriverExportUtils.convertTimestamp(censusTimestamp1))
+ .isEqualTo(
+ com.google.protobuf.Timestamp.newBuilder().setSeconds(100).setNanos(3000).build());
+
+ // Stackdriver doesn't allow negative values, instead it will replace the negative values
+ // by returning a default instance.
+ Timestamp censusTimestamp2 = Timestamp.create(-100, 3000);
+ assertThat(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+ .isEqualTo(com.google.protobuf.Timestamp.newBuilder().build());
+ }
+
+ @Test
+ public void createTimeInterval_cumulative() {
+ Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+ Timestamp censusTimestamp2 = Timestamp.create(200, 0);
+ assertThat(
+ StackdriverExportUtils.createTimeInterval(
+ CumulativeData.create(censusTimestamp1, censusTimestamp2), DISTRIBUTION))
+ .isEqualTo(
+ TimeInterval.newBuilder()
+ .setStartTime(StackdriverExportUtils.convertTimestamp(censusTimestamp1))
+ .setEndTime(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+ .build());
+ assertThat(
+ StackdriverExportUtils.createTimeInterval(
+ CumulativeData.create(censusTimestamp1, censusTimestamp2), LAST_VALUE))
+ .isEqualTo(
+ TimeInterval.newBuilder()
+ .setEndTime(StackdriverExportUtils.convertTimestamp(censusTimestamp2))
+ .build());
+ }
+
+ @Test
+ public void createTimeInterval_interval() {
+ IntervalData intervalData = IntervalData.create(Timestamp.create(200, 0));
+ // Only Cumulative view will supported in this version.
+ thrown.expect(IllegalArgumentException.class);
+ StackdriverExportUtils.createTimeInterval(intervalData, SUM);
+ }
+
+ @Test
+ public void createBucketOptions() {
+ assertThat(StackdriverExportUtils.createBucketOptions(BUCKET_BOUNDARIES))
+ .isEqualTo(
+ BucketOptions.newBuilder()
+ .setExplicitBuckets(
+ Explicit.newBuilder().addAllBounds(Arrays.asList(0.0, 1.0, 3.0, 5.0)))
+ .build());
+ }
+
+ @Test
+ public void createDistribution() {
+ DistributionData distributionData =
+ DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+ assertThat(StackdriverExportUtils.createDistribution(distributionData, BUCKET_BOUNDARIES))
+ .isEqualTo(
+ com.google.api.Distribution.newBuilder()
+ .setMean(2)
+ .setCount(3)
+ // TODO(songya): uncomment this once Stackdriver supports setting max and min.
+ // .setRange(
+ // com.google.api.Distribution.Range.newBuilder().setMin(0).setMax(5).build())
+ .setBucketOptions(StackdriverExportUtils.createBucketOptions(BUCKET_BOUNDARIES))
+ .addAllBucketCounts(Arrays.asList(0L, 1L, 1L, 0L, 1L))
+ .setSumOfSquaredDeviation(14)
+ .build());
+ }
+
+ @Test
+ public void createTypedValue() {
+ assertThat(StackdriverExportUtils.createTypedValue(SUM, SumDataDouble.create(1.1)))
+ .isEqualTo(TypedValue.newBuilder().setDoubleValue(1.1).build());
+ assertThat(StackdriverExportUtils.createTypedValue(SUM, SumDataLong.create(10000)))
+ .isEqualTo(TypedValue.newBuilder().setInt64Value(10000).build());
+ assertThat(StackdriverExportUtils.createTypedValue(COUNT, CountData.create(55)))
+ .isEqualTo(TypedValue.newBuilder().setInt64Value(55).build());
+ assertThat(StackdriverExportUtils.createTypedValue(MEAN, MeanData.create(7.7, 8)))
+ .isEqualTo(TypedValue.newBuilder().setDoubleValue(7.7).build());
+ DistributionData distributionData =
+ DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+ assertThat(StackdriverExportUtils.createTypedValue(DISTRIBUTION, distributionData))
+ .isEqualTo(
+ TypedValue.newBuilder()
+ .setDistributionValue(
+ StackdriverExportUtils.createDistribution(distributionData, BUCKET_BOUNDARIES))
+ .build());
+ assertThat(StackdriverExportUtils.createTypedValue(LAST_VALUE, LastValueDataDouble.create(9.9)))
+ .isEqualTo(TypedValue.newBuilder().setDoubleValue(9.9).build());
+ assertThat(StackdriverExportUtils.createTypedValue(LAST_VALUE, LastValueDataLong.create(90000)))
+ .isEqualTo(TypedValue.newBuilder().setInt64Value(90000).build());
+ }
+
+ @Test
+ public void createPoint_cumulative() {
+ Timestamp censusTimestamp1 = Timestamp.create(100, 3000);
+ Timestamp censusTimestamp2 = Timestamp.create(200, 0);
+ CumulativeData cumulativeData = CumulativeData.create(censusTimestamp1, censusTimestamp2);
+ SumDataDouble sumDataDouble = SumDataDouble.create(33.3);
+
+ assertThat(StackdriverExportUtils.createPoint(sumDataDouble, cumulativeData, SUM))
+ .isEqualTo(
+ Point.newBuilder()
+ .setInterval(StackdriverExportUtils.createTimeInterval(cumulativeData, SUM))
+ .setValue(StackdriverExportUtils.createTypedValue(SUM, sumDataDouble))
+ .build());
+ }
+
+ @Test
+ public void createPoint_interval() {
+ IntervalData intervalData = IntervalData.create(Timestamp.create(200, 0));
+ SumDataDouble sumDataDouble = SumDataDouble.create(33.3);
+ // Only Cumulative view will supported in this version.
+ thrown.expect(IllegalArgumentException.class);
+ StackdriverExportUtils.createPoint(sumDataDouble, intervalData, SUM);
+ }
+
+ @Test
+ public void createMetricDescriptor_cumulative() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ MetricDescriptor metricDescriptor =
+ StackdriverExportUtils.createMetricDescriptor(
+ view, PROJECT_ID, "custom.googleapis.com/myorg/", "myorg/");
+ assertThat(metricDescriptor.getName())
+ .isEqualTo(
+ "projects/"
+ + PROJECT_ID
+ + "/metricDescriptors/custom.googleapis.com/myorg/"
+ + VIEW_NAME);
+ assertThat(metricDescriptor.getDescription()).isEqualTo(VIEW_DESCRIPTION);
+ assertThat(metricDescriptor.getDisplayName()).isEqualTo("myorg/" + VIEW_NAME);
+ assertThat(metricDescriptor.getType()).isEqualTo("custom.googleapis.com/myorg/" + VIEW_NAME);
+ assertThat(metricDescriptor.getUnit()).isEqualTo(MEASURE_UNIT);
+ assertThat(metricDescriptor.getMetricKind()).isEqualTo(MetricKind.CUMULATIVE);
+ assertThat(metricDescriptor.getValueType()).isEqualTo(MetricDescriptor.ValueType.DISTRIBUTION);
+ assertThat(metricDescriptor.getLabelsList())
+ .containsExactly(
+ LabelDescriptor.newBuilder()
+ .setKey(KEY.getName())
+ .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build(),
+ LabelDescriptor.newBuilder()
+ .setKey(StackdriverExportUtils.OPENCENSUS_TASK)
+ .setDescription(StackdriverExportUtils.OPENCENSUS_TASK_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build());
+ }
+
+ @Test
+ public void createMetricDescriptor_cumulative_count() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ COUNT,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ MetricDescriptor metricDescriptor =
+ StackdriverExportUtils.createMetricDescriptor(
+ view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX);
+ assertThat(metricDescriptor.getName())
+ .isEqualTo(
+ "projects/"
+ + PROJECT_ID
+ + "/metricDescriptors/custom.googleapis.com/opencensus/"
+ + VIEW_NAME);
+ assertThat(metricDescriptor.getDescription()).isEqualTo(VIEW_DESCRIPTION);
+ assertThat(metricDescriptor.getDisplayName()).isEqualTo("OpenCensus/" + VIEW_NAME);
+ assertThat(metricDescriptor.getType())
+ .isEqualTo("custom.googleapis.com/opencensus/" + VIEW_NAME);
+ assertThat(metricDescriptor.getUnit()).isEqualTo("1");
+ assertThat(metricDescriptor.getMetricKind()).isEqualTo(MetricKind.CUMULATIVE);
+ assertThat(metricDescriptor.getValueType()).isEqualTo(MetricDescriptor.ValueType.INT64);
+ assertThat(metricDescriptor.getLabelsList())
+ .containsExactly(
+ LabelDescriptor.newBuilder()
+ .setKey(KEY.getName())
+ .setDescription(StackdriverExportUtils.LABEL_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build(),
+ LabelDescriptor.newBuilder()
+ .setKey(StackdriverExportUtils.OPENCENSUS_TASK)
+ .setDescription(StackdriverExportUtils.OPENCENSUS_TASK_DESCRIPTION)
+ .setValueType(ValueType.STRING)
+ .build());
+ }
+
+ @Test
+ public void createMetricDescriptor_interval() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ INTERVAL);
+ assertThat(
+ StackdriverExportUtils.createMetricDescriptor(
+ view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX))
+ .isNull();
+ }
+
+ @Test
+ public void createTimeSeriesList_cumulative() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ DistributionData distributionData1 =
+ DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L));
+ DistributionData distributionData2 =
+ DistributionData.create(-1, 1, -1, -1, 0, Arrays.asList(1L, 0L, 0L, 0L, 0L));
+ Map<List<TagValue>, DistributionData> aggregationMap =
+ ImmutableMap.of(
+ Arrays.asList(VALUE_1), distributionData1, Arrays.asList(VALUE_2), distributionData2);
+ CumulativeData cumulativeData =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ ViewData viewData = ViewData.create(view, aggregationMap, cumulativeData);
+ List<TimeSeries> timeSeriesList =
+ StackdriverExportUtils.createTimeSeriesList(
+ viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
+ assertThat(timeSeriesList).hasSize(2);
+ TimeSeries expected1 =
+ TimeSeries.newBuilder()
+ .setMetricKind(MetricKind.CUMULATIVE)
+ .setValueType(MetricDescriptor.ValueType.DISTRIBUTION)
+ .setMetric(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+ .setResource(MonitoredResource.newBuilder().setType("global"))
+ .addPoints(
+ StackdriverExportUtils.createPoint(distributionData1, cumulativeData, DISTRIBUTION))
+ .build();
+ TimeSeries expected2 =
+ TimeSeries.newBuilder()
+ .setMetricKind(MetricKind.CUMULATIVE)
+ .setValueType(MetricDescriptor.ValueType.DISTRIBUTION)
+ .setMetric(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_2), CUSTOM_OPENCENSUS_DOMAIN))
+ .setResource(MonitoredResource.newBuilder().setType("global"))
+ .addPoints(
+ StackdriverExportUtils.createPoint(distributionData2, cumulativeData, DISTRIBUTION))
+ .build();
+ assertThat(timeSeriesList).containsExactly(expected1, expected2);
+ }
+
+ @Test
+ public void createTimeSeriesList_interval() {
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ INTERVAL);
+ Map<List<TagValue>, DistributionData> aggregationMap =
+ ImmutableMap.of(
+ Arrays.asList(VALUE_1),
+ DistributionData.create(2, 3, 0, 5, 14, Arrays.asList(0L, 1L, 1L, 0L, 1L)),
+ Arrays.asList(VALUE_2),
+ DistributionData.create(-1, 1, -1, -1, 0, Arrays.asList(1L, 0L, 0L, 0L, 0L)));
+ ViewData viewData =
+ ViewData.create(view, aggregationMap, IntervalData.create(Timestamp.fromMillis(2000)));
+ assertThat(
+ StackdriverExportUtils.createTimeSeriesList(
+ viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN))
+ .isEmpty();
+ }
+
+ @Test
+ public void createTimeSeriesList_withCustomMonitoredResource() {
+ MonitoredResource resource =
+ MonitoredResource.newBuilder().setType("global").putLabels("key", "value").build();
+ View view =
+ View.create(
+ Name.create(VIEW_NAME),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ SUM,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ SumDataDouble sumData = SumDataDouble.create(55.5);
+ Map<List<TagValue>, SumDataDouble> aggregationMap =
+ ImmutableMap.of(Arrays.asList(VALUE_1), sumData);
+ CumulativeData cumulativeData =
+ CumulativeData.create(Timestamp.fromMillis(1000), Timestamp.fromMillis(2000));
+ ViewData viewData = ViewData.create(view, aggregationMap, cumulativeData);
+ List<TimeSeries> timeSeriesList =
+ StackdriverExportUtils.createTimeSeriesList(viewData, resource, CUSTOM_OPENCENSUS_DOMAIN);
+ assertThat(timeSeriesList)
+ .containsExactly(
+ TimeSeries.newBuilder()
+ .setMetricKind(MetricKind.CUMULATIVE)
+ .setValueType(MetricDescriptor.ValueType.DOUBLE)
+ .setMetric(
+ StackdriverExportUtils.createMetric(
+ view, Arrays.asList(VALUE_1), CUSTOM_OPENCENSUS_DOMAIN))
+ .setResource(resource)
+ .addPoints(StackdriverExportUtils.createPoint(sumData, cumulativeData, SUM))
+ .build());
+ }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java
new file mode 100644
index 00000000..27593829
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExporterWorkerTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN;
+import static io.opencensus.exporter.stats.stackdriver.StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.api.MetricDescriptor;
+import com.google.api.MonitoredResource;
+import com.google.api.gax.rpc.UnaryCallable;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.cloud.monitoring.v3.stub.MetricServiceStub;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.monitoring.v3.CreateMetricDescriptorRequest;
+import com.google.monitoring.v3.CreateTimeSeriesRequest;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.protobuf.Empty;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure.MeasureLong;
+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.CumulativeData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link StackdriverExporterWorker}. */
+@RunWith(JUnit4.class)
+public class StackdriverExporterWorkerTest {
+
+ private static final String PROJECT_ID = "projectId";
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+ private static final TagKey KEY = TagKey.create("KEY");
+ private static final TagValue VALUE = TagValue.create("VALUE");
+ private static final String MEASURE_NAME = "my measurement";
+ private static final String MEASURE_UNIT = "us";
+ private static final String MEASURE_DESCRIPTION = "measure description";
+ private static final MeasureLong MEASURE =
+ MeasureLong.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT);
+ private static final Name VIEW_NAME = Name.create("my view");
+ private static final String VIEW_DESCRIPTION = "view description";
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+ private static final Interval INTERVAL = Interval.create(ONE_SECOND);
+ private static final Sum SUM = Sum.create();
+ private static final MonitoredResource DEFAULT_RESOURCE =
+ MonitoredResource.newBuilder().setType("global").build();
+
+ @Mock private ViewManager mockViewManager;
+
+ @Mock private MetricServiceStub mockStub;
+
+ @Mock
+ private UnaryCallable<CreateMetricDescriptorRequest, MetricDescriptor>
+ mockCreateMetricDescriptorCallable;
+
+ @Mock private UnaryCallable<CreateTimeSeriesRequest, Empty> mockCreateTimeSeriesCallable;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ doReturn(mockCreateMetricDescriptorCallable).when(mockStub).createMetricDescriptorCallable();
+ doReturn(mockCreateTimeSeriesCallable).when(mockStub).createTimeSeriesCallable();
+ doReturn(null)
+ .when(mockCreateMetricDescriptorCallable)
+ .call(any(CreateMetricDescriptorRequest.class));
+ doReturn(null).when(mockCreateTimeSeriesCallable).call(any(CreateTimeSeriesRequest.class));
+ }
+
+ @Test
+ public void testConstants() {
+ assertThat(StackdriverExporterWorker.MAX_BATCH_EXPORT_SIZE).isEqualTo(200);
+ assertThat(StackdriverExporterWorker.CUSTOM_METRIC_DOMAIN).isEqualTo("custom.googleapis.com/");
+ assertThat(StackdriverExporterWorker.CUSTOM_OPENCENSUS_DOMAIN)
+ .isEqualTo("custom.googleapis.com/opencensus/");
+ assertThat(StackdriverExporterWorker.DEFAULT_DISPLAY_NAME_PREFIX).isEqualTo("OpenCensus/");
+ }
+
+ @Test
+ public void export() throws IOException {
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ ViewData viewData =
+ ViewData.create(
+ view,
+ ImmutableMap.of(Arrays.asList(VALUE), SumDataLong.create(1)),
+ CumulativeData.create(Timestamp.fromMillis(100), Timestamp.fromMillis(200)));
+ doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+ doReturn(viewData).when(mockViewManager).getView(VIEW_NAME);
+
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+ worker.export();
+
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ verify(mockStub, times(1)).createTimeSeriesCallable();
+
+ MetricDescriptor descriptor =
+ StackdriverExportUtils.createMetricDescriptor(
+ view, PROJECT_ID, CUSTOM_OPENCENSUS_DOMAIN, DEFAULT_DISPLAY_NAME_PREFIX);
+ List<TimeSeries> timeSeries =
+ StackdriverExportUtils.createTimeSeriesList(
+ viewData, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
+ verify(mockCreateMetricDescriptorCallable, times(1))
+ .call(
+ eq(
+ CreateMetricDescriptorRequest.newBuilder()
+ .setName("projects/" + PROJECT_ID)
+ .setMetricDescriptor(descriptor)
+ .build()));
+ verify(mockCreateTimeSeriesCallable, times(1))
+ .call(
+ eq(
+ CreateTimeSeriesRequest.newBuilder()
+ .setName("projects/" + PROJECT_ID)
+ .addAllTimeSeries(timeSeries)
+ .build()));
+ }
+
+ @Test
+ public void doNotExportForEmptyViewData() {
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ ViewData empty =
+ ViewData.create(
+ view,
+ Collections.<List<TagValue>, AggregationData>emptyMap(),
+ CumulativeData.create(Timestamp.fromMillis(100), Timestamp.fromMillis(200)));
+ doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+ doReturn(empty).when(mockViewManager).getView(VIEW_NAME);
+
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+
+ worker.export();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ verify(mockStub, times(0)).createTimeSeriesCallable();
+ }
+
+ @Test
+ public void doNotExportIfFailedToRegisterView() {
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ doReturn(ImmutableSet.of(view)).when(mockViewManager).getAllExportedViews();
+ doThrow(new IllegalArgumentException()).when(mockStub).createMetricDescriptorCallable();
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+
+ assertThat(worker.registerView(view)).isFalse();
+ worker.export();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ verify(mockStub, times(0)).createTimeSeriesCallable();
+ }
+
+ @Test
+ public void skipDifferentViewWithSameName() throws IOException {
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+ View view1 =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ assertThat(worker.registerView(view1)).isTrue();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+
+ View view2 =
+ View.create(
+ VIEW_NAME,
+ "This is a different description.",
+ MEASURE,
+ SUM,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ assertThat(worker.registerView(view2)).isFalse();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ }
+
+ @Test
+ public void doNotCreateMetricDescriptorForRegisteredView() {
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), CUMULATIVE);
+ assertThat(worker.registerView(view)).isTrue();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+
+ assertThat(worker.registerView(view)).isTrue();
+ verify(mockStub, times(1)).createMetricDescriptorCallable();
+ }
+
+ @Test
+ public void doNotCreateMetricDescriptorForIntervalView() {
+ StackdriverExporterWorker worker =
+ new StackdriverExporterWorker(
+ PROJECT_ID,
+ new FakeMetricServiceClient(mockStub),
+ ONE_SECOND,
+ mockViewManager,
+ DEFAULT_RESOURCE,
+ null);
+ View view =
+ View.create(VIEW_NAME, VIEW_DESCRIPTION, MEASURE, SUM, Arrays.asList(KEY), INTERVAL);
+ assertThat(worker.registerView(view)).isFalse();
+ verify(mockStub, times(0)).createMetricDescriptorCallable();
+ }
+
+ @Test
+ public void getDomain() {
+ assertThat(StackdriverExporterWorker.getDomain(null))
+ .isEqualTo("custom.googleapis.com/opencensus/");
+ assertThat(StackdriverExporterWorker.getDomain(""))
+ .isEqualTo("custom.googleapis.com/opencensus/");
+ assertThat(StackdriverExporterWorker.getDomain("custom.googleapis.com/myorg/"))
+ .isEqualTo("custom.googleapis.com/myorg/");
+ assertThat(StackdriverExporterWorker.getDomain("external.googleapis.com/prometheus/"))
+ .isEqualTo("external.googleapis.com/prometheus/");
+ assertThat(StackdriverExporterWorker.getDomain("myorg")).isEqualTo("myorg/");
+ }
+
+ @Test
+ public void getDisplayNamePrefix() {
+ assertThat(StackdriverExporterWorker.getDisplayNamePrefix(null)).isEqualTo("OpenCensus/");
+ assertThat(StackdriverExporterWorker.getDisplayNamePrefix("")).isEqualTo("");
+ assertThat(StackdriverExporterWorker.getDisplayNamePrefix("custom.googleapis.com/myorg/"))
+ .isEqualTo("custom.googleapis.com/myorg/");
+ assertThat(
+ StackdriverExporterWorker.getDisplayNamePrefix("external.googleapis.com/prometheus/"))
+ .isEqualTo("external.googleapis.com/prometheus/");
+ assertThat(StackdriverExporterWorker.getDisplayNamePrefix("myorg")).isEqualTo("myorg/");
+ }
+
+ /*
+ * MetricServiceClient.createMetricDescriptor() and MetricServiceClient.createTimeSeries() are
+ * final methods and cannot be mocked. We have to use a mock MetricServiceStub in order to verify
+ * the output.
+ */
+ private static final class FakeMetricServiceClient extends MetricServiceClient {
+
+ protected FakeMetricServiceClient(MetricServiceStub stub) {
+ super(stub);
+ }
+ }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.java
new file mode 100644
index 00000000..2d5eba1b
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsConfigurationTest.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.exporter.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.MonitoredResource;
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import io.opencensus.common.Duration;
+import java.util.Date;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverStatsConfiguration}. */
+@RunWith(JUnit4.class)
+public class StackdriverStatsConfigurationTest {
+
+ private static final Credentials FAKE_CREDENTIALS =
+ GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+ private static final String PROJECT_ID = "project";
+ private static final Duration DURATION = Duration.create(10, 0);
+ private static final MonitoredResource RESOURCE =
+ MonitoredResource.newBuilder()
+ .setType("gce-instance")
+ .putLabels("instance-id", "instance")
+ .build();
+ private static final String CUSTOM_PREFIX = "myorg";
+
+ @Test
+ public void testBuild() {
+ StackdriverStatsConfiguration configuration =
+ StackdriverStatsConfiguration.builder()
+ .setCredentials(FAKE_CREDENTIALS)
+ .setProjectId(PROJECT_ID)
+ .setExportInterval(DURATION)
+ .setMonitoredResource(RESOURCE)
+ .setMetricNamePrefix(CUSTOM_PREFIX)
+ .build();
+ assertThat(configuration.getCredentials()).isEqualTo(FAKE_CREDENTIALS);
+ assertThat(configuration.getProjectId()).isEqualTo(PROJECT_ID);
+ assertThat(configuration.getExportInterval()).isEqualTo(DURATION);
+ assertThat(configuration.getMonitoredResource()).isEqualTo(RESOURCE);
+ assertThat(configuration.getMetricNamePrefix()).isEqualTo(CUSTOM_PREFIX);
+ }
+
+ @Test
+ public void testBuild_Default() {
+ StackdriverStatsConfiguration configuration = StackdriverStatsConfiguration.builder().build();
+ assertThat(configuration.getCredentials()).isNull();
+ assertThat(configuration.getProjectId()).isNull();
+ assertThat(configuration.getExportInterval()).isNull();
+ assertThat(configuration.getMonitoredResource()).isNull();
+ assertThat(configuration.getMetricNamePrefix()).isNull();
+ }
+}
diff --git a/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java
new file mode 100644
index 00000000..f5e3edd5
--- /dev/null
+++ b/exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverStatsExporterTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.exporter.stats.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import io.opencensus.common.Duration;
+import java.io.IOException;
+import java.util.Date;
+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 StackdriverStatsExporter}. */
+@RunWith(JUnit4.class)
+public class StackdriverStatsExporterTest {
+
+ private static final String PROJECT_ID = "projectId";
+ private static final Duration ONE_SECOND = Duration.create(1, 0);
+ private static final Duration NEG_ONE_SECOND = Duration.create(-1, 0);
+ private static final Credentials FAKE_CREDENTIALS =
+ GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+ private static final StackdriverStatsConfiguration CONFIGURATION =
+ StackdriverStatsConfiguration.builder()
+ .setCredentials(FAKE_CREDENTIALS)
+ .setProjectId("project")
+ .build();
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testConstants() {
+ assertThat(StackdriverStatsExporter.DEFAULT_INTERVAL).isEqualTo(Duration.create(60, 0));
+ }
+
+ @Test
+ public void createWithNullStackdriverStatsConfiguration() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("configuration");
+ StackdriverStatsExporter.createAndRegister((StackdriverStatsConfiguration) null);
+ }
+
+ @Test
+ public void createWithNegativeDuration_WithConfiguration() throws IOException {
+ StackdriverStatsConfiguration configuration =
+ StackdriverStatsConfiguration.builder()
+ .setCredentials(FAKE_CREDENTIALS)
+ .setExportInterval(NEG_ONE_SECOND)
+ .build();
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Duration must be positive");
+ StackdriverStatsExporter.createAndRegister(configuration);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNullCredentials() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("credentials");
+ StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+ null, PROJECT_ID, ONE_SECOND);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNullProjectId() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("projectId");
+ StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+ GoogleCredentials.newBuilder().build(), null, ONE_SECOND);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNullDuration() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("exportInterval");
+ StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+ GoogleCredentials.newBuilder().build(), PROJECT_ID, null);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNegativeDuration() throws IOException {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Duration must be positive");
+ StackdriverStatsExporter.createAndRegisterWithCredentialsAndProjectId(
+ GoogleCredentials.newBuilder().build(), PROJECT_ID, NEG_ONE_SECOND);
+ }
+
+ @Test
+ public void createExporterTwice() throws IOException {
+ StackdriverStatsExporter.createAndRegister(CONFIGURATION);
+ try {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Stackdriver stats exporter is already created.");
+ StackdriverStatsExporter.createAndRegister(CONFIGURATION);
+ } finally {
+ StackdriverStatsExporter.unsafeResetExporter();
+ }
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void createWithNullMonitoredResource() throws IOException {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("monitoredResource");
+ StackdriverStatsExporter.createAndRegisterWithMonitoredResource(ONE_SECOND, null);
+ }
+}
diff --git a/exporters/trace/instana/README.md b/exporters/trace/instana/README.md
new file mode 100644
index 00000000..22ace227
--- /dev/null
+++ b/exporters/trace/instana/README.md
@@ -0,0 +1,73 @@
+# OpenCensus Instana Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Instana Trace Exporter* is a trace exporter that exports
+data to Instana. [Instana](http://www.instana.com/) is a distributed
+tracing system.
+
+## Quickstart
+
+### Prerequisites
+
+[Instana](http://www.instana.com/) forwards traces exported by applications
+instrumented with Census to its backend using the Instana agent processes as proxy.
+If the agent is used on the same host as Census, please take care to deactivate
+automatic tracing.
+
+
+### Hello Stan
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-instana</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-instana:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ InstanaTraceExporter.createAndRegister("http://localhost:42699/com.instana.plugin.generic.trace");
+ // ...
+ }
+}
+```
+
+#### Java Versions
+
+Java 6 or above is required for using this exporter.
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-instana/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-instana
diff --git a/exporters/trace/instana/build.gradle b/exporters/trace/instana/build.gradle
new file mode 100644
index 00000000..028bc208
--- /dev/null
+++ b/exporters/trace/instana/build.gradle
@@ -0,0 +1,16 @@
+description = 'OpenCensus Trace Instana Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java
new file mode 100644
index 00000000..649a026f
--- /dev/null
+++ b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaExporterHandler.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2018, 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.exporter.trace.instana;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.io.BaseEncoding;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/*
+ * Exports to an Instana agent acting as proxy to the Instana backend (and handling authentication)
+ * Uses the Trace SDK documented:
+ * https://github.com/instana/instana-java-sdk#instana-trace-webservice
+ *
+ * Currently does a blocking export using HttpUrlConnection.
+ * Also uses a StringBuilder to build JSON.
+ * Both can be improved should 3rd party library usage not be a concern.
+ *
+ * Major TODO is the limitation of Instana to only suport 64bit trace ids, which will be resolved.
+ * Until then it is crossing fingers and treating it as 50% sampler :).
+ */
+final class InstanaExporterHandler extends SpanExporter.Handler {
+
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySpampler = Samplers.probabilitySampler(0.0001);
+ private final URL agentEndpoint;
+
+ InstanaExporterHandler(URL agentEndpoint) {
+ this.agentEndpoint = agentEndpoint;
+ }
+
+ private static String encodeTraceId(TraceId traceId) {
+ return BaseEncoding.base16().lowerCase().encode(traceId.getBytes(), 0, 8);
+ }
+
+ private static String encodeSpanId(SpanId spanId) {
+ return BaseEncoding.base16().lowerCase().encode(spanId.getBytes());
+ }
+
+ private static String toSpanName(SpanData spanData) {
+ return spanData.getName();
+ }
+
+ private static String toSpanType(SpanData spanData) {
+ if (spanData.getKind() == Kind.SERVER
+ || (spanData.getKind() == null
+ && (spanData.getParentSpanId() == null
+ || Boolean.TRUE.equals(spanData.getHasRemoteParent())))) {
+ return "ENTRY";
+ }
+
+ // This is a hack because the Span API did not have SpanKind.
+ if (spanData.getKind() == Kind.CLIENT
+ || (spanData.getKind() == null && spanData.getName().startsWith("Sent."))) {
+ return "EXIT";
+ }
+
+ return "INTERMEDIATE";
+ }
+
+ private static long toMillis(Timestamp timestamp) {
+ return SECONDS.toMillis(timestamp.getSeconds()) + NANOSECONDS.toMillis(timestamp.getNanos());
+ }
+
+ private static long toMillis(Timestamp start, Timestamp end) {
+ Duration duration = end.subtractTimestamp(start);
+ return SECONDS.toMillis(duration.getSeconds()) + NANOSECONDS.toMillis(duration.getNanos());
+ }
+
+ // The return type needs to be nullable when this function is used as an argument to 'match' in
+ // attributeValueToString, because 'match' doesn't allow covariant return types.
+ private static final Function<Object, /*@Nullable*/ String> returnToString =
+ Functions.returnToString();
+
+ @javax.annotation.Nullable
+ private static String attributeValueToString(AttributeValue attributeValue) {
+ return attributeValue.match(
+ returnToString,
+ returnToString,
+ returnToString,
+ returnToString,
+ Functions.</*@Nullable*/ String>returnNull());
+ }
+
+ static String convertToJson(Collection<SpanData> spanDataList) {
+ StringBuilder sb = new StringBuilder();
+ sb.append('[');
+ for (final SpanData span : spanDataList) {
+ final SpanContext spanContext = span.getContext();
+ final SpanId parentSpanId = span.getParentSpanId();
+ final Timestamp startTimestamp = span.getStartTimestamp();
+ final Timestamp endTimestamp = span.getEndTimestamp();
+ final Status status = span.getStatus();
+ if (status == null || endTimestamp == null) {
+ continue;
+ }
+ if (sb.length() > 1) {
+ sb.append(',');
+ }
+ sb.append('{');
+ sb.append("\"spanId\":\"").append(encodeSpanId(spanContext.getSpanId())).append("\",");
+ sb.append("\"traceId\":\"").append(encodeTraceId(spanContext.getTraceId())).append("\",");
+ if (parentSpanId != null) {
+ sb.append("\"parentId\":\"").append(encodeSpanId(parentSpanId)).append("\",");
+ }
+ sb.append("\"timestamp\":").append(toMillis(startTimestamp)).append(',');
+ sb.append("\"duration\":").append(toMillis(startTimestamp, endTimestamp)).append(',');
+ sb.append("\"name\":\"").append(toSpanName(span)).append("\",");
+ sb.append("\"type\":\"").append(toSpanType(span)).append('"');
+ if (!status.isOk()) {
+ sb.append(",\"error\":").append("true");
+ }
+ Map<String, AttributeValue> attributeMap = span.getAttributes().getAttributeMap();
+ if (attributeMap.size() > 0) {
+ StringBuilder dataSb = new StringBuilder();
+ dataSb.append('{');
+ for (Entry<String, AttributeValue> entry : attributeMap.entrySet()) {
+ if (dataSb.length() > 1) {
+ dataSb.append(',');
+ }
+ dataSb
+ .append("\"")
+ .append(entry.getKey())
+ .append("\":\"")
+ .append(attributeValueToString(entry.getValue()))
+ .append("\"");
+ }
+ dataSb.append('}');
+
+ sb.append(",\"data\":").append(dataSb);
+ }
+ sb.append('}');
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+ // sets the default sampler to always sample and we get the gRPC span of the instana
+ // export call always sampled and go to an infinite loop.
+ Scope scope =
+ tracer.spanBuilder("ExportInstanaTraces").setSampler(probabilitySpampler).startScopedSpan();
+ try {
+ String json = convertToJson(spanDataList);
+
+ OutputStream outputStream = null;
+ InputStream inputStream = null;
+ try {
+ HttpURLConnection connection = (HttpURLConnection) agentEndpoint.openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+ outputStream = connection.getOutputStream();
+ outputStream.write(json.getBytes(Charset.defaultCharset()));
+ outputStream.flush();
+ inputStream = connection.getInputStream();
+ if (connection.getResponseCode() != 200) {
+ tracer
+ .getCurrentSpan()
+ .setStatus(
+ Status.UNKNOWN.withDescription("Response " + connection.getResponseCode()));
+ }
+ } catch (IOException e) {
+ tracer
+ .getCurrentSpan()
+ .setStatus(
+ Status.UNKNOWN.withDescription(
+ e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+ // dropping span batch
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java
new file mode 100644
index 00000000..da2ce354
--- /dev/null
+++ b/exporters/trace/instana/src/main/java/io/opencensus/exporter/trace/instana/InstanaTraceExporter.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2018, 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.exporter.trace.instana;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.net.MalformedURLException;
+import java.net.URL;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Instana.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * InstanaTraceExporter.createAndRegister("http://localhost:42699/com.instana.plugin.generic.trace");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class InstanaTraceExporter {
+
+ private static final String REGISTER_NAME = InstanaTraceExporter.class.getName();
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static Handler handler = null;
+
+ private InstanaTraceExporter() {}
+
+ /**
+ * Creates and registers the Instana Trace exporter to the OpenCensus library. Only one Instana
+ * exporter can be registered at any point.
+ *
+ * @param agentEndpoint Ex http://localhost:42699/com.instana.plugin.generic.trace
+ * @throws MalformedURLException if the agentEndpoint is not a valid http url.
+ * @throws IllegalStateException if a Instana exporter is already registered.
+ * @since 0.12
+ */
+ public static void createAndRegister(String agentEndpoint) throws MalformedURLException {
+ synchronized (monitor) {
+ checkState(handler == null, "Instana exporter is already registered.");
+ Handler newHandler = new InstanaExporterHandler(new URL(agentEndpoint));
+ handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Registers the {@code InstanaTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ /**
+ * Unregisters the Instana Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Instana exporter is not registered.
+ * @since 0.12
+ */
+ public static void unregister() {
+ synchronized (monitor) {
+ checkState(handler != null, "Instana exporter is not registered.");
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ handler = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@code InstanaTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+}
diff --git a/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java
new file mode 100644
index 00000000..3b5e119e
--- /dev/null
+++ b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaExporterHandlerTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2018, 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.exporter.trace.instana;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.Attributes;
+import io.opencensus.trace.export.SpanData.Links;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InstanaExporterHandler}. */
+@RunWith(JUnit4.class)
+public class InstanaExporterHandlerTest {
+ private static final String TRACE_ID = "d239036e7d5cec116b562147388b35bf";
+ private static final String SPAN_ID = "9cc1e3049173be09";
+ private static final String PARENT_SPAN_ID = "8b03ab423da481c5";
+ private static final Map<String, AttributeValue> attributes =
+ ImmutableMap.of("http.url", AttributeValue.stringAttributeValue("http://localhost/foo"));
+ private static final List<TimedEvent<Annotation>> annotations = Collections.emptyList();
+ private static final List<TimedEvent<MessageEvent>> messageEvents =
+ ImmutableList.of(
+ TimedEvent.create(
+ Timestamp.create(1505855799, 433901068),
+ MessageEvent.builder(Type.RECEIVED, 0).setCompressedMessageSize(7).build()),
+ TimedEvent.create(
+ Timestamp.create(1505855799, 459486280),
+ MessageEvent.builder(Type.SENT, 0).setCompressedMessageSize(13).build()));
+
+ @Test
+ public void generateSpan_NoKindAndRemoteParent() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "SpanName", /* name */
+ null, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+ .isEqualTo(
+ "["
+ + "{"
+ + "\"spanId\":\"9cc1e3049173be09\","
+ + "\"traceId\":\"d239036e7d5cec11\","
+ + "\"parentId\":\"8b03ab423da481c5\","
+ + "\"timestamp\":1505855794194,"
+ + "\"duration\":5271,"
+ + "\"name\":\"SpanName\","
+ + "\"type\":\"ENTRY\","
+ + "\"data\":"
+ + "{\"http.url\":\"http://localhost/foo\"}"
+ + "}"
+ + "]");
+ }
+
+ @Test
+ public void generateSpan_ServerKind() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "SpanName", /* name */
+ Kind.SERVER, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+ .isEqualTo(
+ "["
+ + "{"
+ + "\"spanId\":\"9cc1e3049173be09\","
+ + "\"traceId\":\"d239036e7d5cec11\","
+ + "\"parentId\":\"8b03ab423da481c5\","
+ + "\"timestamp\":1505855794194,"
+ + "\"duration\":5271,"
+ + "\"name\":\"SpanName\","
+ + "\"type\":\"ENTRY\","
+ + "\"data\":"
+ + "{\"http.url\":\"http://localhost/foo\"}"
+ + "}"
+ + "]");
+ }
+
+ @Test
+ public void generateSpan_ClientKind() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "SpanName", /* name */
+ Kind.CLIENT, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(InstanaExporterHandler.convertToJson(Collections.singletonList(data)))
+ .isEqualTo(
+ "["
+ + "{"
+ + "\"spanId\":\"9cc1e3049173be09\","
+ + "\"traceId\":\"d239036e7d5cec11\","
+ + "\"parentId\":\"8b03ab423da481c5\","
+ + "\"timestamp\":1505855794194,"
+ + "\"duration\":5271,"
+ + "\"name\":\"SpanName\","
+ + "\"type\":\"EXIT\","
+ + "\"data\":"
+ + "{\"http.url\":\"http://localhost/foo\"}"
+ + "}"
+ + "]");
+ }
+}
diff --git a/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java
new file mode 100644
index 00000000..a4d03df3
--- /dev/null
+++ b/exporters/trace/instana/src/test/java/io/opencensus/exporter/trace/instana/InstanaTraceExporterTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.exporter.trace.instana;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link InstanaTraceExporter}. */
+@RunWith(JUnit4.class)
+public class InstanaTraceExporterTest {
+
+ @Mock private SpanExporter spanExporter;
+ @Mock private Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterInstanaExporter() {
+ InstanaTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.instana.InstanaTraceExporter"), same(handler));
+ InstanaTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.instana.InstanaTraceExporter"));
+ }
+}
diff --git a/exporters/trace/jaeger/README.md b/exporters/trace/jaeger/README.md
new file mode 100644
index 00000000..7a5b68eb
--- /dev/null
+++ b/exporters/trace/jaeger/README.md
@@ -0,0 +1,90 @@
+# OpenCensus Jaeger Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Jaeger Trace Exporter* is a trace exporter that exports
+data to Jaeger.
+
+[Jaeger](https://jaeger.readthedocs.io/en/latest/), inspired by [Dapper](https://research.google.com/pubs/pub36356.html) and [OpenZipkin](http://zipkin.io/), is a distributed tracing system released as open source by [Uber Technologies](http://uber.github.io/). It is used for monitoring and troubleshooting microservices-based distributed systems, including:
+
+- Distributed context propagation
+- Distributed transaction monitoring
+- Root cause analysis
+- Service dependency analysis
+- Performance / latency optimization
+
+## Quickstart
+
+### Prerequisites
+
+[Jaeger](https://jaeger.readthedocs.io/en/latest/) stores and queries traces exported by
+applications instrumented with Census. The easiest way to [start a Jaeger
+server](https://jaeger.readthedocs.io/en/latest/getting_started/) is to paste the below:
+
+```bash
+docker run -d \
+ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
+ -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp \
+ -p5778:5778 -p16686:16686 -p14268:14268 -p9411:9411 \
+ jaegertracing/all-in-one:latest
+```
+
+### Hello Jaeger
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-jaeger</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-jaeger:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This will export traces to the Jaeger thrift format to the Jaeger instance started previously:
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ JaegerTraceExporter.createAndRegister("http://127.0.0.1:14268/api/traces", "my-service");
+ // ...
+ }
+}
+```
+
+See also [this integration test](https://github.com/census-instrumentation/opencensus-java/blob/master/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java).
+
+#### Java Versions
+
+Java 6 or above is required for using this exporter.
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-jaeger/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-jaeger
diff --git a/exporters/trace/jaeger/build.gradle b/exporters/trace/jaeger/build.gradle
new file mode 100644
index 00000000..04829aa4
--- /dev/null
+++ b/exporters/trace/jaeger/build.gradle
@@ -0,0 +1,37 @@
+description = 'OpenCensus Trace Jaeger Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+// Docker tests require JDK 8+
+sourceSets {
+ test {
+ java {
+ if (!JavaVersion.current().isJava8Compatible()) {
+ exclude '**/JaegerExporterHandlerIntegrationTest.java'
+ }
+ }
+ }
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ compile(libraries.jaeger_reporter) {
+ // Prefer library version.
+ exclude group: 'com.google.guava', module: 'guava'
+ }
+
+ testCompile project(':opencensus-api'),
+ 'org.testcontainers:testcontainers:1.7.0',
+ 'com.google.http-client:google-http-client-gson:1.23.0'
+
+ // Unless linked to impl, spans will be blank and not exported during integration tests.
+ testRuntime project(':opencensus-impl')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java
new file mode 100644
index 00000000..e0a16296
--- /dev/null
+++ b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandler.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.lang.String.format;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+import com.google.errorprone.annotations.MustBeClosed;
+import com.uber.jaeger.exceptions.SenderException;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Log;
+import com.uber.jaeger.thriftjava.Process;
+import com.uber.jaeger.thriftjava.Span;
+import com.uber.jaeger.thriftjava.SpanRef;
+import com.uber.jaeger.thriftjava.SpanRefType;
+import com.uber.jaeger.thriftjava.Tag;
+import com.uber.jaeger.thriftjava.TagType;
+import io.opencensus.common.Function;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+@NotThreadSafe
+final class JaegerExporterHandler extends SpanExporter.Handler {
+ private static final String EXPORT_SPAN_NAME = "ExportJaegerTraces";
+ private static final String DESCRIPTION = "description";
+
+ private static final Logger logger = Logger.getLogger(JaegerExporterHandler.class.getName());
+
+ /**
+ * Sampler with low probability used during the export in order to avoid the case when user sets
+ * the default sampler to always sample and we get the Thrift span of the Jaeger export call
+ * always sampled and go to an infinite loop.
+ */
+ private static final Sampler lowProbabilitySampler = Samplers.probabilitySampler(0.0001);
+
+ private static final Tracer tracer = Tracing.getTracer();
+
+ private static final Function<? super String, Tag> stringAttributeConverter =
+ new Function<String, Tag>() {
+ @Override
+ public Tag apply(final String value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.STRING);
+ tag.setVStr(value);
+ return tag;
+ }
+ };
+
+ private static final Function<? super Boolean, Tag> booleanAttributeConverter =
+ new Function<Boolean, Tag>() {
+ @Override
+ public Tag apply(final Boolean value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.BOOL);
+ tag.setVBool(value);
+ return tag;
+ }
+ };
+
+ private static final Function<? super Double, Tag> doubleAttributeConverter =
+ new Function<Double, Tag>() {
+ @Override
+ public Tag apply(final Double value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.DOUBLE);
+ tag.setVDouble(value);
+ return tag;
+ }
+ };
+
+ private static final Function<? super Long, Tag> longAttributeConverter =
+ new Function<Long, Tag>() {
+ @Override
+ public Tag apply(final Long value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.LONG);
+ tag.setVLong(value);
+ return tag;
+ }
+ };
+
+ private static final Function<Object, Tag> defaultAttributeConverter =
+ new Function<Object, Tag>() {
+ @Override
+ public Tag apply(final Object value) {
+ final Tag tag = new Tag();
+ tag.setVType(TagType.STRING);
+ tag.setVStr(value.toString());
+ return tag;
+ }
+ };
+
+ // Re-usable buffers to avoid too much memory allocation during conversions.
+ // N.B.: these make instances of this class thread-unsafe, hence the above
+ // @NotThreadSafe annotation.
+ private final byte[] spanIdBuffer = new byte[SpanId.SIZE];
+ private final byte[] traceIdBuffer = new byte[TraceId.SIZE];
+ private final byte[] optionsBuffer = new byte[Integer.SIZE / Byte.SIZE];
+
+ private final HttpSender sender;
+ private final Process process;
+
+ JaegerExporterHandler(final HttpSender sender, final Process process) {
+ this.sender = checkNotNull(sender, "Jaeger sender must NOT be null.");
+ this.process = checkNotNull(process, "Process sending traces must NOT be null.");
+ }
+
+ @Override
+ public void export(final Collection<SpanData> spanDataList) {
+ final Scope exportScope = newExportScope();
+ try {
+ doExport(spanDataList);
+ } catch (SenderException e) {
+ tracer
+ .getCurrentSpan() // exportScope above.
+ .setStatus(Status.UNKNOWN.withDescription(getMessageOrDefault(e)));
+ logger.log(Level.WARNING, "Failed to export traces to Jaeger: " + e);
+ } finally {
+ exportScope.close();
+ }
+ }
+
+ @MustBeClosed
+ private static Scope newExportScope() {
+ // Start a new span with explicit sampler (with low probability) to avoid the case when user
+ // sets the default sampler to always sample and we get the Thrift span of the Jaeger
+ // export call always sampled and go to an infinite loop.
+ return tracer.spanBuilder(EXPORT_SPAN_NAME).setSampler(lowProbabilitySampler).startScopedSpan();
+ }
+
+ private void doExport(final Collection<SpanData> spanDataList) throws SenderException {
+ final List<Span> spans = spanDataToJaegerThriftSpans(spanDataList);
+ sender.send(process, spans);
+ }
+
+ private static String getMessageOrDefault(final SenderException e) {
+ return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
+ }
+
+ private List<Span> spanDataToJaegerThriftSpans(final Collection<SpanData> spanDataList) {
+ final List<Span> spans = Lists.newArrayListWithExpectedSize(spanDataList.size());
+ for (final SpanData spanData : spanDataList) {
+ spans.add(spanDataToJaegerThriftSpan(spanData));
+ }
+ return spans;
+ }
+
+ private Span spanDataToJaegerThriftSpan(final SpanData spanData) {
+ final long startTimeInMicros = timestampToMicros(spanData.getStartTimestamp());
+ final long endTimeInMicros = timestampToMicros(spanData.getEndTimestamp());
+
+ final SpanContext context = spanData.getContext();
+ copyToBuffer(context.getTraceId());
+
+ return new com.uber.jaeger.thriftjava.Span(
+ traceIdLow(),
+ traceIdHigh(),
+ spanIdToLong(context.getSpanId()),
+ spanIdToLong(spanData.getParentSpanId()),
+ spanData.getName(),
+ optionsToFlags(context.getTraceOptions()),
+ startTimeInMicros,
+ endTimeInMicros - startTimeInMicros)
+ .setReferences(linksToReferences(spanData.getLinks().getLinks()))
+ .setTags(attributesToTags(spanData.getAttributes().getAttributeMap()))
+ .setLogs(annotationEventsToLogs(spanData.getAnnotations().getEvents()));
+ }
+
+ private void copyToBuffer(final TraceId traceId) {
+ // Attempt to minimise allocations, since TraceId#getBytes currently creates a defensive copy:
+ traceId.copyBytesTo(traceIdBuffer, 0);
+ }
+
+ private long traceIdHigh() {
+ return Longs.fromBytes(
+ traceIdBuffer[0],
+ traceIdBuffer[1],
+ traceIdBuffer[2],
+ traceIdBuffer[3],
+ traceIdBuffer[4],
+ traceIdBuffer[5],
+ traceIdBuffer[6],
+ traceIdBuffer[7]);
+ }
+
+ private long traceIdLow() {
+ return Longs.fromBytes(
+ traceIdBuffer[8],
+ traceIdBuffer[9],
+ traceIdBuffer[10],
+ traceIdBuffer[11],
+ traceIdBuffer[12],
+ traceIdBuffer[13],
+ traceIdBuffer[14],
+ traceIdBuffer[15]);
+ }
+
+ private long spanIdToLong(final @Nullable SpanId spanId) {
+ if (spanId == null) {
+ return 0L;
+ }
+ // Attempt to minimise allocations, since SpanId#getBytes currently creates a defensive copy:
+ spanId.copyBytesTo(spanIdBuffer, 0);
+ return Longs.fromByteArray(spanIdBuffer);
+ }
+
+ private int optionsToFlags(final TraceOptions traceOptions) {
+ // Attempt to minimise allocations, since TraceOptions#getBytes currently creates a defensive
+ // copy:
+ traceOptions.copyBytesTo(optionsBuffer, optionsBuffer.length - 1);
+ return Ints.fromByteArray(optionsBuffer);
+ }
+
+ private List<SpanRef> linksToReferences(final List<Link> links) {
+ final List<SpanRef> spanRefs = Lists.newArrayListWithExpectedSize(links.size());
+ for (final Link link : links) {
+ copyToBuffer(link.getTraceId());
+ spanRefs.add(
+ new SpanRef(
+ linkTypeToRefType(link.getType()),
+ traceIdLow(),
+ traceIdHigh(),
+ spanIdToLong(link.getSpanId())));
+ }
+ return spanRefs;
+ }
+
+ private static long timestampToMicros(final @Nullable Timestamp timestamp) {
+ return (timestamp == null)
+ ? 0L
+ : SECONDS.toMicros(timestamp.getSeconds()) + NANOSECONDS.toMicros(timestamp.getNanos());
+ }
+
+ private static SpanRefType linkTypeToRefType(final Link.Type type) {
+ switch (type) {
+ case CHILD_LINKED_SPAN:
+ return SpanRefType.CHILD_OF;
+ case PARENT_LINKED_SPAN:
+ return SpanRefType.FOLLOWS_FROM;
+ }
+ throw new UnsupportedOperationException(
+ format("Failed to convert link type [%s] to a Jaeger SpanRefType.", type));
+ }
+
+ private static List<Tag> attributesToTags(final Map<String, AttributeValue> attributes) {
+ final List<Tag> tags = Lists.newArrayListWithExpectedSize(attributes.size());
+ for (final Map.Entry<String, AttributeValue> entry : attributes.entrySet()) {
+ final Tag tag =
+ entry
+ .getValue()
+ .match(
+ stringAttributeConverter,
+ booleanAttributeConverter,
+ longAttributeConverter,
+ doubleAttributeConverter,
+ defaultAttributeConverter);
+ tag.setKey(entry.getKey());
+ tags.add(tag);
+ }
+ return tags;
+ }
+
+ private static List<Log> annotationEventsToLogs(
+ final List<SpanData.TimedEvent<Annotation>> events) {
+ final List<Log> logs = Lists.newArrayListWithExpectedSize(events.size());
+ for (final SpanData.TimedEvent<Annotation> event : events) {
+ final long timestampsInMicros = timestampToMicros(event.getTimestamp());
+ final List<Tag> tags = attributesToTags(event.getEvent().getAttributes());
+ tags.add(descriptionToTag(event.getEvent().getDescription()));
+ final Log log = new Log(timestampsInMicros, tags);
+ logs.add(log);
+ }
+ return logs;
+ }
+
+ private static Tag descriptionToTag(final String description) {
+ final Tag tag = new Tag(DESCRIPTION, TagType.STRING);
+ tag.setVStr(description);
+ return tag;
+ }
+}
diff --git a/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java
new file mode 100644
index 00000000..4890f01a
--- /dev/null
+++ b/exporters/trace/jaeger/src/main/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporter.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Process;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Jaeger. Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * JaegerTraceExporter.createAndRegister("http://127.0.0.1:14268/api/traces", "myservicename");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.13
+ */
+public final class JaegerTraceExporter {
+ private static final String REGISTER_NAME = JaegerTraceExporter.class.getName();
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static SpanExporter.Handler handler = null;
+
+ // Make constructor private to hide it from the API and therefore avoid users calling it.
+ private JaegerTraceExporter() {}
+
+ /**
+ * Creates and registers the Jaeger Trace exporter to the OpenCensus library. Only one Jaeger
+ * exporter can be registered at any point.
+ *
+ * @param thriftEndpoint the Thrift endpoint of your Jaeger instance, e.g.:
+ * "http://127.0.0.1:14268/api/traces"
+ * @param serviceName the local service name of the process.
+ * @throws IllegalStateException if a Jaeger exporter is already registered.
+ * @since 0.13
+ */
+ public static void createAndRegister(final String thriftEndpoint, final String serviceName) {
+ synchronized (monitor) {
+ checkState(handler == null, "Jaeger exporter is already registered.");
+ final SpanExporter.Handler newHandler = newHandler(thriftEndpoint, serviceName);
+ JaegerTraceExporter.handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Creates and registers the Jaeger Trace exporter to the OpenCensus library using the provided
+ * HttpSender. Only one Jaeger exporter can be registered at any point.
+ *
+ * @param httpSender the pre-configured HttpSender to use with the exporter
+ * @param serviceName the local service name of the process.
+ * @throws IllegalStateException if a Jaeger exporter is already registered.
+ * @since 0.17
+ */
+ public static void createWithSender(final HttpSender httpSender, final String serviceName) {
+ synchronized (monitor) {
+ checkState(handler == null, "Jaeger exporter is already registered.");
+ final SpanExporter.Handler newHandler = newHandlerWithSender(httpSender, serviceName);
+ JaegerTraceExporter.handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ private static SpanExporter.Handler newHandler(
+ final String thriftEndpoint, final String serviceName) {
+ final HttpSender sender = new HttpSender(thriftEndpoint);
+ final Process process = new Process(serviceName);
+ return new JaegerExporterHandler(sender, process);
+ }
+
+ private static SpanExporter.Handler newHandlerWithSender(
+ final HttpSender sender, final String serviceName) {
+ final Process process = new Process(serviceName);
+ return new JaegerExporterHandler(sender, process);
+ }
+
+ /**
+ * Registers the {@link JaegerTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(final SpanExporter spanExporter, final SpanExporter.Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ /**
+ * Unregisters the {@link JaegerTraceExporter} from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Jaeger exporter is not registered.
+ * @since 0.13
+ */
+ public static void unregister() {
+ synchronized (monitor) {
+ checkState(handler != null, "Jaeger exporter is not registered.");
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ handler = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@link JaegerTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@link SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(final SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java
new file mode 100644
index 00000000..9d6a7976
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerIntegrationTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import io.opencensus.common.Scope;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.util.Random;
+import org.junit.AfterClass;
+import org.junit.AssumptionViolatedException;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+
+public class JaegerExporterHandlerIntegrationTest {
+ private static final String JAEGER_IMAGE = "jaegertracing/all-in-one:1.3";
+ private static final int JAEGER_HTTP_PORT = 16686;
+ private static final int JAEGER_HTTP_PORT_THRIFT = 14268;
+ private static final String SERVICE_NAME = "test";
+ private static final String SPAN_NAME = "my.org/ProcessVideo";
+ private static final String START_PROCESSING_VIDEO = "Start processing video.";
+ private static final String FINISHED_PROCESSING_VIDEO = "Finished processing video.";
+
+ private static final Logger logger =
+ LoggerFactory.getLogger(JaegerExporterHandlerIntegrationTest.class);
+
+ private final HttpRequestFactory httpRequestFactory =
+ new NetHttpTransport().createRequestFactory();
+
+ private static GenericContainer<?> container;
+
+ /** Starts a docker container optionally. For example, skips if Docker is unavailable. */
+ @SuppressWarnings("rawtypes")
+ @BeforeClass
+ public static void startContainer() {
+ try {
+ container =
+ new GenericContainer(JAEGER_IMAGE)
+ .withExposedPorts(JAEGER_HTTP_PORT, JAEGER_HTTP_PORT_THRIFT)
+ .waitingFor(new HttpWaitStrategy());
+ container.start();
+ } catch (RuntimeException e) {
+ throw new AssumptionViolatedException("could not start docker container", e);
+ }
+ }
+
+ @AfterClass
+ public static void stopContainer() {
+ if (container != null) {
+ container.stop();
+ }
+ }
+
+ @Before
+ public void before() {
+ JaegerTraceExporter.createAndRegister(thriftTracesEndpoint(), SERVICE_NAME);
+ }
+
+ @Test
+ public void exportToJaeger() throws InterruptedException, IOException {
+ Tracer tracer = Tracing.getTracer();
+ final long startTimeInMillis = currentTimeMillis();
+
+ SpanBuilder spanBuilder =
+ tracer.spanBuilder(SPAN_NAME).setRecordEvents(true).setSampler(Samplers.alwaysSample());
+ int spanDurationInMillis = new Random().nextInt(10) + 1;
+
+ Scope scopedSpan = spanBuilder.startScopedSpan();
+ try {
+ tracer.getCurrentSpan().addAnnotation(START_PROCESSING_VIDEO);
+ Thread.sleep(spanDurationInMillis); // Fake work.
+ tracer.getCurrentSpan().putAttribute("foo", AttributeValue.stringAttributeValue("bar"));
+ tracer.getCurrentSpan().addAnnotation(FINISHED_PROCESSING_VIDEO);
+ } catch (Exception e) {
+ tracer.getCurrentSpan().addAnnotation("Exception thrown when processing video.");
+ tracer.getCurrentSpan().setStatus(Status.UNKNOWN);
+ logger.error(e.getMessage());
+ } finally {
+ scopedSpan.close();
+ }
+
+ logger.info("Wait longer than the reporting duration...");
+ // Wait for a duration longer than reporting duration (5s) to ensure spans are exported.
+ long timeWaitingForSpansToBeExportedInMillis = 5100L;
+ Thread.sleep(timeWaitingForSpansToBeExportedInMillis);
+ JaegerTraceExporter.unregister();
+ final long endTimeInMillis = currentTimeMillis();
+
+ // Get traces recorded by Jaeger:
+ HttpRequest request =
+ httpRequestFactory.buildGetRequest(new GenericUrl(tracesForServiceEndpoint(SERVICE_NAME)));
+ HttpResponse response = request.execute();
+ String body = response.parseAsString();
+ assertWithMessage("Response was: " + body).that(response.getStatusCode()).isEqualTo(200);
+
+ JsonObject result = new JsonParser().parse(body).getAsJsonObject();
+ // Pretty-print for debugging purposes:
+ logger.debug(new GsonBuilder().setPrettyPrinting().create().toJson(result));
+
+ assertThat(result).isNotNull();
+ assertThat(result.get("total").getAsInt()).isEqualTo(0);
+ assertThat(result.get("limit").getAsInt()).isEqualTo(0);
+ assertThat(result.get("offset").getAsInt()).isEqualTo(0);
+ assertThat(result.get("errors").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+ JsonArray data = result.get("data").getAsJsonArray();
+ assertThat(data).isNotNull();
+ assertThat(data.size()).isEqualTo(1);
+ JsonObject trace = data.get(0).getAsJsonObject();
+ assertThat(trace).isNotNull();
+ assertThat(trace.get("traceID").getAsString()).matches("[a-z0-9]{1,32}");
+
+ JsonArray spans = trace.get("spans").getAsJsonArray();
+ assertThat(spans).isNotNull();
+ assertThat(spans.size()).isEqualTo(1);
+
+ JsonObject span = spans.get(0).getAsJsonObject();
+ assertThat(span).isNotNull();
+ assertThat(span.get("traceID").getAsString()).matches("[a-z0-9]{1,32}");
+ assertThat(span.get("spanID").getAsString()).matches("[a-z0-9]{1,16}");
+ assertThat(span.get("flags").getAsInt()).isEqualTo(1);
+ assertThat(span.get("operationName").getAsString()).isEqualTo(SPAN_NAME);
+ assertThat(span.get("references").getAsJsonArray()).isEmpty();
+ assertThat(span.get("startTime").getAsLong())
+ .isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+ assertThat(span.get("startTime").getAsLong()).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+ assertThat(span.get("duration").getAsLong())
+ .isAtLeast(MILLISECONDS.toMicros(spanDurationInMillis));
+ assertThat(span.get("duration").getAsLong())
+ .isAtMost(
+ MILLISECONDS.toMicros(spanDurationInMillis + timeWaitingForSpansToBeExportedInMillis));
+
+ JsonArray tags = span.get("tags").getAsJsonArray();
+ assertThat(tags.size()).isEqualTo(1);
+ JsonObject tag = tags.get(0).getAsJsonObject();
+ assertThat(tag.get("key").getAsString()).isEqualTo("foo");
+ assertThat(tag.get("type").getAsString()).isEqualTo("string");
+ assertThat(tag.get("value").getAsString()).isEqualTo("bar");
+
+ JsonArray logs = span.get("logs").getAsJsonArray();
+ assertThat(logs.size()).isEqualTo(2);
+
+ JsonObject log1 = logs.get(0).getAsJsonObject();
+ long ts1 = log1.get("timestamp").getAsLong();
+ assertThat(ts1).isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+ assertThat(ts1).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+ JsonArray fields1 = log1.get("fields").getAsJsonArray();
+ assertThat(fields1.size()).isEqualTo(1);
+ JsonObject field1 = fields1.get(0).getAsJsonObject();
+ assertThat(field1.get("key").getAsString()).isEqualTo("description");
+ assertThat(field1.get("type").getAsString()).isEqualTo("string");
+ assertThat(field1.get("value").getAsString()).isEqualTo(START_PROCESSING_VIDEO);
+
+ JsonObject log2 = logs.get(1).getAsJsonObject();
+ long ts2 = log2.get("timestamp").getAsLong();
+ assertThat(ts2).isAtLeast(MILLISECONDS.toMicros(startTimeInMillis));
+ assertThat(ts2).isAtMost(MILLISECONDS.toMicros(endTimeInMillis));
+ assertThat(ts2).isAtLeast(ts1);
+ JsonArray fields2 = log2.get("fields").getAsJsonArray();
+ assertThat(fields2.size()).isEqualTo(1);
+ JsonObject field2 = fields2.get(0).getAsJsonObject();
+ assertThat(field2.get("key").getAsString()).isEqualTo("description");
+ assertThat(field2.get("type").getAsString()).isEqualTo("string");
+ assertThat(field2.get("value").getAsString()).isEqualTo(FINISHED_PROCESSING_VIDEO);
+
+ assertThat(span.get("processID").getAsString()).isEqualTo("p1");
+ assertThat(span.get("warnings").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+
+ JsonObject processes = trace.get("processes").getAsJsonObject();
+ assertThat(processes.size()).isEqualTo(1);
+ JsonObject p1 = processes.get("p1").getAsJsonObject();
+ assertThat(p1.get("serviceName").getAsString()).isEqualTo(SERVICE_NAME);
+ assertThat(p1.get("tags").getAsJsonArray().size()).isEqualTo(0);
+ assertThat(trace.get("warnings").getAsJsonNull()).isEqualTo(JsonNull.INSTANCE);
+ }
+
+ private static String thriftTracesEndpoint() {
+ return format(
+ "http://%s:%s/api/traces",
+ container.getContainerIpAddress(), container.getMappedPort(JAEGER_HTTP_PORT_THRIFT));
+ }
+
+ private static String tracesForServiceEndpoint(String service) {
+ return format(
+ "http://%s:%s/api/traces?service=%s",
+ container.getContainerIpAddress(), container.getMappedPort(JAEGER_HTTP_PORT), service);
+ }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java
new file mode 100644
index 00000000..f918f015
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerExporterHandlerTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.uber.jaeger.exceptions.SenderException;
+import com.uber.jaeger.senders.HttpSender;
+import com.uber.jaeger.thriftjava.Log;
+import com.uber.jaeger.thriftjava.Process;
+import com.uber.jaeger.thriftjava.Span;
+import com.uber.jaeger.thriftjava.SpanRef;
+import com.uber.jaeger.thriftjava.SpanRefType;
+import com.uber.jaeger.thriftjava.Tag;
+import com.uber.jaeger.thriftjava.TagType;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class JaegerExporterHandlerTest {
+ private static final byte FF = (byte) 0xFF;
+
+ private final HttpSender mockSender = mock(HttpSender.class);
+ private final Process process = new Process("test");
+ private final JaegerExporterHandler handler = new JaegerExporterHandler(mockSender, process);
+
+ @Captor private ArgumentCaptor<List<Span>> captor;
+
+ @Test
+ public void exportShouldConvertFromSpanDataToJaegerThriftSpan() throws SenderException {
+ final long startTime = 1519629870001L;
+ final long endTime = 1519630148002L;
+ final SpanData spanData =
+ SpanData.create(
+ sampleSpanContext(),
+ SpanId.fromBytes(new byte[] {(byte) 0x7F, FF, FF, FF, FF, FF, FF, FF}),
+ true,
+ "test",
+ Timestamp.fromMillis(startTime),
+ SpanData.Attributes.create(sampleAttributes(), 0),
+ SpanData.TimedEvents.create(singletonList(sampleAnnotation()), 0),
+ SpanData.TimedEvents.create(singletonList(sampleMessageEvent()), 0),
+ SpanData.Links.create(sampleLinks(), 0),
+ 0,
+ Status.OK,
+ Timestamp.fromMillis(endTime));
+
+ handler.export(singletonList(spanData));
+
+ verify(mockSender).send(eq(process), captor.capture());
+ List<Span> spans = captor.getValue();
+
+ assertThat(spans.size()).isEqualTo(1);
+ Span span = spans.get(0);
+
+ assertThat(span.operationName).isEqualTo("test");
+ assertThat(span.spanId).isEqualTo(256L);
+ assertThat(span.traceIdHigh).isEqualTo(-72057594037927936L);
+ assertThat(span.traceIdLow).isEqualTo(1L);
+ assertThat(span.parentSpanId).isEqualTo(Long.MAX_VALUE);
+ assertThat(span.flags).isEqualTo(1);
+ assertThat(span.startTime).isEqualTo(MILLISECONDS.toMicros(startTime));
+ assertThat(span.duration).isEqualTo(MILLISECONDS.toMicros(endTime - startTime));
+
+ assertThat(span.tags.size()).isEqualTo(3);
+ assertThat(span.tags)
+ .containsExactly(
+ new Tag("BOOL", TagType.BOOL).setVBool(false),
+ new Tag("LONG", TagType.LONG).setVLong(Long.MAX_VALUE),
+ new Tag("STRING", TagType.STRING)
+ .setVStr(
+ "Judge of a man by his questions rather than by his answers. -- Voltaire"));
+
+ assertThat(span.logs.size()).isEqualTo(1);
+ Log log = span.logs.get(0);
+ assertThat(log.timestamp).isEqualTo(1519629872987654L);
+ assertThat(log.fields.size()).isEqualTo(4);
+ assertThat(log.fields)
+ .containsExactly(
+ new Tag("description", TagType.STRING).setVStr("annotation #1"),
+ new Tag("bool", TagType.BOOL).setVBool(true),
+ new Tag("long", TagType.LONG).setVLong(1337L),
+ new Tag("string", TagType.STRING)
+ .setVStr("Kind words do not cost much. Yet they accomplish much. -- Pascal"));
+
+ assertThat(span.references.size()).isEqualTo(1);
+ SpanRef reference = span.references.get(0);
+ assertThat(reference.traceIdHigh).isEqualTo(-1L);
+ assertThat(reference.traceIdLow).isEqualTo(-256L);
+ assertThat(reference.spanId).isEqualTo(512L);
+ assertThat(reference.refType).isEqualTo(SpanRefType.CHILD_OF);
+ }
+
+ private static SpanContext sampleSpanContext() {
+ return SpanContext.create(
+ TraceId.fromBytes(new byte[] {FF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}),
+ SpanId.fromBytes(new byte[] {0, 0, 0, 0, 0, 0, 1, 0}),
+ TraceOptions.builder().setIsSampled(true).build());
+ }
+
+ private static ImmutableMap<String, AttributeValue> sampleAttributes() {
+ return ImmutableMap.of(
+ "BOOL", AttributeValue.booleanAttributeValue(false),
+ "LONG", AttributeValue.longAttributeValue(Long.MAX_VALUE),
+ "STRING",
+ AttributeValue.stringAttributeValue(
+ "Judge of a man by his questions rather than by his answers. -- Voltaire"));
+ }
+
+ private static SpanData.TimedEvent<Annotation> sampleAnnotation() {
+ return SpanData.TimedEvent.create(
+ Timestamp.create(1519629872L, 987654321),
+ Annotation.fromDescriptionAndAttributes(
+ "annotation #1",
+ ImmutableMap.of(
+ "bool", AttributeValue.booleanAttributeValue(true),
+ "long", AttributeValue.longAttributeValue(1337L),
+ "string",
+ AttributeValue.stringAttributeValue(
+ "Kind words do not cost much. Yet they accomplish much. -- Pascal"))));
+ }
+
+ private static SpanData.TimedEvent<MessageEvent> sampleMessageEvent() {
+ return SpanData.TimedEvent.create(
+ Timestamp.create(1519629871L, 123456789),
+ MessageEvent.builder(MessageEvent.Type.SENT, 42L).build());
+ }
+
+ private static List<Link> sampleLinks() {
+ return Lists.newArrayList(
+ Link.fromSpanContext(
+ SpanContext.create(
+ TraceId.fromBytes(
+ new byte[] {FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, FF, 0}),
+ SpanId.fromBytes(new byte[] {0, 0, 0, 0, 0, 0, 2, 0}),
+ TraceOptions.builder().setIsSampled(false).build()),
+ Link.Type.CHILD_LINKED_SPAN,
+ ImmutableMap.of(
+ "Bool", AttributeValue.booleanAttributeValue(true),
+ "Long", AttributeValue.longAttributeValue(299792458L),
+ "String",
+ AttributeValue.stringAttributeValue(
+ "Man is condemned to be free; because once thrown into the world, "
+ + "he is responsible for everything he does. -- Sartre"))));
+ }
+}
diff --git a/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java
new file mode 100644
index 00000000..c00b0133
--- /dev/null
+++ b/exporters/trace/jaeger/src/test/java/io/opencensus/exporter/trace/jaeger/JaegerTraceExporterTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018, 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.exporter.trace.jaeger;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class JaegerTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+
+ @Mock private SpanExporter.Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterJaegerExporter() {
+ JaegerTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.jaeger.JaegerTraceExporter"), same(handler));
+ JaegerTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.jaeger.JaegerTraceExporter"));
+ }
+}
diff --git a/exporters/trace/logging/README.md b/exporters/trace/logging/README.md
new file mode 100644
index 00000000..51f2566d
--- /dev/null
+++ b/exporters/trace/logging/README.md
@@ -0,0 +1,57 @@
+# OpenCensus Logging Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Logging trace exporter* is a trace exporter that logs all data to the system log.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-logging</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-logging:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ LoggingTraceExporter.register();
+ // ...
+ }
+}
+```
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-logging/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-logging
diff --git a/exporters/trace/logging/build.gradle b/exporters/trace/logging/build.gradle
new file mode 100644
index 00000000..a7fb0ff6
--- /dev/null
+++ b/exporters/trace/logging/build.gradle
@@ -0,0 +1,11 @@
+description = 'OpenCensus Trace Logging Exporter'
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+} \ No newline at end of file
diff --git a/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java
new file mode 100644
index 00000000..46f01ffc
--- /dev/null
+++ b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingExporter.java
@@ -0,0 +1,81 @@
+/*
+ * 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.exporter.trace.logging;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An OpenCensus span exporter implementation which logs all data.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * LoggingExporter.register();
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link LoggingTraceExporter}.
+ * @since 0.6
+ */
+@ThreadSafe
+@Deprecated
+public final class LoggingExporter {
+ private LoggingExporter() {}
+
+ /**
+ * Registers the Logging exporter to the OpenCensus library.
+ *
+ * @since 0.6
+ */
+ public static void register() {
+ LoggingTraceExporter.register();
+ }
+
+ /**
+ * Registers the {@code LoggingHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter) {
+ LoggingTraceExporter.register(spanExporter);
+ }
+
+ /**
+ * Unregisters the Logging exporter from the OpenCensus library.
+ *
+ * @since 0.6
+ */
+ public static void unregister() {
+ LoggingTraceExporter.unregister();
+ }
+
+ /**
+ * Unregisters the {@code LoggingHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ LoggingTraceExporter.unregister(spanExporter);
+ }
+}
diff --git a/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java
new file mode 100644
index 00000000..9267e201
--- /dev/null
+++ b/exporters/trace/logging/src/main/java/io/opencensus/exporter/trace/logging/LoggingTraceExporter.java
@@ -0,0 +1,101 @@
+/*
+ * 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.exporter.trace.logging;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An OpenCensus span exporter implementation which logs all data.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * LoggingTraceExporter.register();
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+@ThreadSafe
+public final class LoggingTraceExporter {
+ private static final Logger logger = Logger.getLogger(LoggingTraceExporter.class.getName());
+ private static final String REGISTER_NAME = LoggingTraceExporter.class.getName();
+ private static final LoggingExporterHandler HANDLER = new LoggingExporterHandler();
+
+ private LoggingTraceExporter() {}
+
+ /**
+ * Registers the Logging exporter to the OpenCensus library.
+ *
+ * @since 0.12
+ */
+ public static void register() {
+ register(Tracing.getExportComponent().getSpanExporter());
+ }
+
+ /**
+ * Registers the {@code LoggingHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter) {
+ spanExporter.registerHandler(REGISTER_NAME, HANDLER);
+ }
+
+ /**
+ * Unregisters the Logging exporter from the OpenCensus library.
+ *
+ * @since 0.12
+ */
+ public static void unregister() {
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ }
+
+ /**
+ * Unregisters the {@code LoggingHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+
+ @VisibleForTesting
+ static final class LoggingExporterHandler extends Handler {
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // TODO(bdrutu): Use JSON as a standard format for logging SpanData and define this to be
+ // compatible between languages.
+ for (SpanData spanData : spanDataList) {
+ logger.log(Level.INFO, spanData.toString());
+ }
+ }
+ }
+}
diff --git a/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java b/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java
new file mode 100644
index 00000000..c2b77e4e
--- /dev/null
+++ b/exporters/trace/logging/src/test/java/io/opencensus/exporter/trace/logging/LoggingTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.exporter.trace.logging;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.exporter.trace.logging.LoggingTraceExporter.LoggingExporterHandler;
+import io.opencensus.trace.export.SpanExporter;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link LoggingTraceExporter}. */
+@RunWith(JUnit4.class)
+public class LoggingTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterLoggingService() {
+ LoggingTraceExporter.register(spanExporter);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.logging.LoggingTraceExporter"),
+ any(LoggingExporterHandler.class));
+ LoggingTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.logging.LoggingTraceExporter"));
+ }
+}
diff --git a/exporters/trace/ocagent/README.md b/exporters/trace/ocagent/README.md
new file mode 100644
index 00000000..4f25bd6e
--- /dev/null
+++ b/exporters/trace/ocagent/README.md
@@ -0,0 +1,48 @@
+# OpenCensus Java OC-Agent Trace Exporter
+
+The *OpenCensus Java OC-Agent Trace Exporter* is the Java implementation of the OpenCensus Agent
+(OC-Agent) Trace Exporter.
+
+## Quickstart
+
+### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.17.0</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-ocagent</artifactId>
+ <version>0.17.0</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.17.0</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```gradle
+compile 'io.opencensus:opencensus-api:0.17.0'
+compile 'io.opencensus:opencensus-exporter-trace-ocagent:0.17.0'
+runtime 'io.opencensus:opencensus-impl:0.17.0'
+```
+
+### Register the exporter
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ OcAgentTraceExporter.createAndRegister();
+ // ...
+ }
+}
+```
diff --git a/exporters/trace/ocagent/build.gradle b/exporters/trace/ocagent/build.gradle
new file mode 100644
index 00000000..777c08d0
--- /dev/null
+++ b/exporters/trace/ocagent/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Java OC-Agent Trace Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ project(':opencensus-contrib-monitored-resource-util'),
+ libraries.grpc_core,
+ libraries.grpc_netty,
+ libraries.grpc_stub,
+ libraries.opencensus_proto
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java
new file mode 100644
index 00000000..65729803
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtils.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.OpenCensusLibraryInformation;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils;
+import io.opencensus.proto.agent.common.v1.LibraryInfo;
+import io.opencensus.proto.agent.common.v1.LibraryInfo.Language;
+import io.opencensus.proto.agent.common.v1.Node;
+import io.opencensus.proto.agent.common.v1.ProcessIdentifier;
+import io.opencensus.proto.agent.common.v1.ServiceInfo;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Utilities for detecting and creating {@link Node}. */
+final class OcAgentNodeUtils {
+
+ // The current version of the OpenCensus OC-Agent Exporter.
+ @VisibleForTesting
+ static final String OC_AGENT_EXPORTER_VERSION = "0.17.0-SNAPSHOT"; // CURRENT_OPENCENSUS_VERSION
+
+ @VisibleForTesting static final String RESOURCE_TYPE_ATTRIBUTE_KEY = "OPENCENSUS_SOURCE_TYPE";
+ @VisibleForTesting static final String RESOURCE_LABEL_ATTRIBUTE_KEY = "OPENCENSUS_SOURCE_LABELS";
+
+ @Nullable
+ private static final MonitoredResource RESOURCE = MonitoredResourceUtils.getDefaultResource();
+
+ // Creates a Node with information from the OpenCensus library and environment variables.
+ static Node getNodeInfo(String serviceName) {
+ String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+ Timestamp censusTimestamp = Timestamp.fromMillis(System.currentTimeMillis());
+ return Node.newBuilder()
+ .setIdentifier(getProcessIdentifier(jvmName, censusTimestamp))
+ .setLibraryInfo(getLibraryInfo(OpenCensusLibraryInformation.VERSION))
+ .setServiceInfo(getServiceInfo(serviceName))
+ .putAllAttributes(getAttributeMap(RESOURCE))
+ .build();
+ }
+
+ // Creates process identifier with the given JVM name and start time.
+ @VisibleForTesting
+ static ProcessIdentifier getProcessIdentifier(String jvmName, Timestamp censusTimestamp) {
+ String hostname;
+ int pid;
+ // jvmName should be something like '<pid>@<hostname>', at least in Oracle and OpenJdk JVMs
+ int delimiterIndex = jvmName.indexOf('@');
+ if (delimiterIndex < 1) {
+ // Not the expected format, generate a random number.
+ try {
+ hostname = InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ hostname = "localhost";
+ }
+ // Generate a random number as the PID.
+ pid = new SecureRandom().nextInt();
+ } else {
+ hostname = jvmName.substring(delimiterIndex + 1, jvmName.length());
+ try {
+ pid = Integer.parseInt(jvmName.substring(0, delimiterIndex));
+ } catch (NumberFormatException e) {
+ // Generate a random number as the PID if format is unexpected.
+ pid = new SecureRandom().nextInt();
+ }
+ }
+
+ return ProcessIdentifier.newBuilder()
+ .setHostName(hostname)
+ .setPid(pid)
+ .setStartTimestamp(TraceProtoUtils.toTimestampProto(censusTimestamp))
+ .build();
+ }
+
+ // Creates library info with the given OpenCensus Java version.
+ @VisibleForTesting
+ static LibraryInfo getLibraryInfo(String currentOcJavaVersion) {
+ return LibraryInfo.newBuilder()
+ .setLanguage(Language.JAVA)
+ .setCoreLibraryVersion(currentOcJavaVersion)
+ .setExporterVersion(OC_AGENT_EXPORTER_VERSION)
+ .build();
+ }
+
+ // Creates service info with the given service name.
+ @VisibleForTesting
+ static ServiceInfo getServiceInfo(String serviceName) {
+ return ServiceInfo.newBuilder().setName(serviceName).build();
+ }
+
+ /*
+ * Creates an attribute map with the given MonitoredResource.
+ * If the given resource is not null, the attribute map contains exactly two entries:
+ *
+ * OPENCENSUS_SOURCE_TYPE:
+ * A string that describes the type of the resource prefixed by a domain namespace,
+ * e.g. “kubernetes.io/container”.
+ * OPENCENSUS_SOURCE_LABELS:
+ * A comma-separated list of labels describing the source in more detail,
+ * e.g. “key1=val1,key2=val2”. The allowed character set is appropriately constrained.
+ */
+ // TODO: update the resource attributes once we have an agreement on the resource specs:
+ // https://github.com/census-instrumentation/opencensus-specs/pull/162.
+ @VisibleForTesting
+ static Map<String, String> getAttributeMap(@Nullable MonitoredResource resource) {
+ if (resource == null) {
+ return Collections.emptyMap();
+ } else {
+ Map<String, String> resourceAttributes = new HashMap<String, String>();
+ resourceAttributes.put(RESOURCE_TYPE_ATTRIBUTE_KEY, resource.getResourceType().name());
+ resourceAttributes.put(RESOURCE_LABEL_ATTRIBUTE_KEY, getConcatenatedResourceLabels(resource));
+ return resourceAttributes;
+ }
+ }
+
+ // Encodes the attributes of MonitoredResource into a comma-separated list of labels.
+ // For example "aws_account=account1,instance_id=instance1,region=us-east-2".
+ private static String getConcatenatedResourceLabels(MonitoredResource resource) {
+ StringBuilder resourceLabels = new StringBuilder();
+ if (resource instanceof AwsEc2InstanceMonitoredResource) {
+ AwsEc2InstanceMonitoredResource awsEc2Resource = (AwsEc2InstanceMonitoredResource) resource;
+ putIntoBuilderIfHasValue(resourceLabels, "aws_account", awsEc2Resource.getAccount());
+ putIntoBuilderIfHasValue(resourceLabels, "instance_id", awsEc2Resource.getInstanceId());
+ putIntoBuilderIfHasValue(resourceLabels, "region", awsEc2Resource.getRegion());
+ } else if (resource instanceof GcpGceInstanceMonitoredResource) {
+ GcpGceInstanceMonitoredResource gceResource = (GcpGceInstanceMonitoredResource) resource;
+ putIntoBuilderIfHasValue(resourceLabels, "gcp_account", gceResource.getAccount());
+ putIntoBuilderIfHasValue(resourceLabels, "instance_id", gceResource.getInstanceId());
+ putIntoBuilderIfHasValue(resourceLabels, "zone", gceResource.getZone());
+ } else if (resource instanceof GcpGkeContainerMonitoredResource) {
+ GcpGkeContainerMonitoredResource gkeResource = (GcpGkeContainerMonitoredResource) resource;
+ putIntoBuilderIfHasValue(resourceLabels, "gcp_account", gkeResource.getAccount());
+ putIntoBuilderIfHasValue(resourceLabels, "instance_id", gkeResource.getInstanceId());
+ putIntoBuilderIfHasValue(resourceLabels, "location", gkeResource.getZone());
+ putIntoBuilderIfHasValue(resourceLabels, "namespace_name", gkeResource.getNamespaceId());
+ putIntoBuilderIfHasValue(resourceLabels, "cluster_name", gkeResource.getClusterName());
+ putIntoBuilderIfHasValue(resourceLabels, "container_name", gkeResource.getContainerName());
+ putIntoBuilderIfHasValue(resourceLabels, "pod_name", gkeResource.getPodId());
+ }
+ return resourceLabels.toString();
+ }
+
+ // If the given resourceValue is not empty, encodes resourceKey and resourceValue as
+ // "resourceKey:resourceValue" and puts it into the given StringBuilder. Otherwise skip the value.
+ private static void putIntoBuilderIfHasValue(
+ StringBuilder builder, String resourceKey, String resourceValue) {
+ if (resourceValue.isEmpty()) {
+ return;
+ }
+ if (!(builder.length() == 0)) {
+ // Appends the comma separator to the front, if the StringBuilder already has entries.
+ builder.append(',');
+ }
+ builder.append(resourceKey);
+ builder.append('=');
+ builder.append(resourceValue);
+ }
+
+ private OcAgentNodeUtils() {}
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java
new file mode 100644
index 00000000..5c468ded
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporter.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * The implementation of the OpenCensus Agent (OC-Agent) Trace Exporter.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * OcAgentTraceExporter.createAndRegister();
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.17
+ */
+@ThreadSafe
+public final class OcAgentTraceExporter {
+
+ private static final Object monitor = new Object();
+ private static final String REGISTER_NAME = OcAgentTraceExporter.class.getName();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static Handler handler = null;
+
+ private OcAgentTraceExporter() {}
+
+ /**
+ * Creates a {@code OcAgentTraceExporterHandler} with default configurations and registers it to
+ * the OpenCensus library.
+ *
+ * @since 0.17
+ */
+ public static void createAndRegister() {
+ synchronized (monitor) {
+ checkState(handler == null, "OC-Agent exporter is already registered.");
+ OcAgentTraceExporterHandler newHandler = new OcAgentTraceExporterHandler();
+ registerInternal(newHandler);
+ }
+ }
+
+ /**
+ * Creates a {@code OcAgentTraceExporterHandler} with the given configurations and registers it to
+ * the OpenCensus library.
+ *
+ * @param configuration the {@code OcAgentTraceExporterConfiguration}.
+ * @since 0.17
+ */
+ public static void createAndRegister(OcAgentTraceExporterConfiguration configuration) {
+ synchronized (monitor) {
+ checkState(handler == null, "OC-Agent exporter is already registered.");
+ OcAgentTraceExporterHandler newHandler =
+ new OcAgentTraceExporterHandler(
+ configuration.getEndPoint(),
+ configuration.getServiceName(),
+ configuration.getUseInsecure(),
+ configuration.getRetryInterval(),
+ configuration.getEnableConfig());
+ registerInternal(newHandler);
+ }
+ }
+
+ /**
+ * Registers the {@code OcAgentTraceExporterHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ private static void registerInternal(Handler newHandler) {
+ synchronized (monitor) {
+ handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Unregisters the OC-Agent exporter from the OpenCensus library.
+ *
+ * @since 0.17
+ */
+ public static void unregister() {
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ }
+
+ /**
+ * Unregisters the {@code OcAgentTraceExporterHandler}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java
new file mode 100644
index 00000000..c7bf1e95
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfiguration.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import com.google.auto.value.AutoValue;
+import io.opencensus.common.Duration;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link OcAgentTraceExporter}.
+ *
+ * @since 0.17
+ */
+@AutoValue
+@Immutable
+public abstract class OcAgentTraceExporterConfiguration {
+
+ OcAgentTraceExporterConfiguration() {}
+
+ /**
+ * Returns the end point of OC-Agent. The end point can be dns, ip:port, etc.
+ *
+ * @return the end point of OC-Agent.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract String getEndPoint();
+
+ /**
+ * Returns whether to disable client transport security for the exporter's gRPC connection or not.
+ *
+ * @return whether to disable client transport security for the exporter's gRPC connection or not.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Boolean getUseInsecure();
+
+ /**
+ * Returns the service name to be used for this {@link OcAgentTraceExporter}.
+ *
+ * @return the service name.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract String getServiceName();
+
+ /**
+ * Returns the retry time interval when trying to connect to Agent.
+ *
+ * @return the retry time interval.
+ * @since 0.17
+ */
+ @Nullable
+ public abstract Duration getRetryInterval();
+
+ /**
+ * Returns whether the {@link OcAgentTraceExporter} should handle the config streams.
+ *
+ * @return whether the {@code OcAgentTraceExporter} should handle the config streams.
+ * @since 0.17
+ */
+ public abstract boolean getEnableConfig();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.17
+ */
+ public static Builder builder() {
+ return new AutoValue_OcAgentTraceExporterConfiguration.Builder().setEnableConfig(true);
+ }
+
+ /**
+ * Builder for {@link OcAgentTraceExporterConfiguration}.
+ *
+ * @since 0.17
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the end point of OC-Agent server.
+ *
+ * @param endPoint the end point of OC-Agent.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setEndPoint(String endPoint);
+
+ /**
+ * Sets whether to disable client transport security for the exporter's gRPC connection or not.
+ *
+ * @param useInsecure whether disable client transport security for the exporter's gRPC
+ * connection.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setUseInsecure(Boolean useInsecure);
+
+ /**
+ * Sets the service name to be used for this {@link OcAgentTraceExporter}.
+ *
+ * @param serviceName the service name.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setServiceName(String serviceName);
+
+ /**
+ * Sets the retry time interval when trying to connect to Agent.
+ *
+ * @param retryInterval the retry time interval.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setRetryInterval(Duration retryInterval);
+
+ /**
+ * Sets whether {@link OcAgentTraceExporter} should handle the config streams.
+ *
+ * @param enableConfig whether {@code OcAgentTraceExporter} should handle the config streams.
+ * @return this.
+ * @since 0.17
+ */
+ public abstract Builder setEnableConfig(boolean enableConfig);
+
+ // TODO(songya): add an option that controls whether to always keep the RPC connection alive.
+
+ /**
+ * Builds a {@link OcAgentTraceExporterConfiguration}.
+ *
+ * @return a {@code OcAgentTraceExporterConfiguration}.
+ * @since 0.17
+ */
+ public abstract OcAgentTraceExporterConfiguration build();
+ }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java
new file mode 100644
index 00000000..5edc06df
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import io.opencensus.common.Duration;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.util.Collection;
+import javax.annotation.Nullable;
+
+/** Exporting handler for OC-Agent Tracing. */
+final class OcAgentTraceExporterHandler extends Handler {
+
+ private static final String DEFAULT_END_POINT = "localhost:55678";
+ private static final String DEFAULT_SERVICE_NAME = "OpenCensus";
+ private static final Duration DEFAULT_RETRY_INTERVAL = Duration.create(300, 0); // 5 minutes
+
+ OcAgentTraceExporterHandler() {
+ this(null, null, null, null, /* enableConfig= */ true);
+ }
+
+ OcAgentTraceExporterHandler(
+ @Nullable String endPoint,
+ @Nullable String serviceName,
+ @Nullable Boolean useInsecure,
+ @Nullable Duration retryInterval,
+ boolean enableConfig) {
+ // if (endPoint == null) {
+ // endPoint = DEFAULT_END_POINT;
+ // }
+ // if (serviceName == null) {
+ // serviceName = DEFAULT_SERVICE_NAME;
+ // }
+ // if (useInsecure == null) {
+ // useInsecure = false;
+ // }
+ // if (retryInterval == null) {
+ // retryInterval = DEFAULT_RETRY_INTERVAL;
+ // }
+ // OcAgentTraceServiceClients.startAttemptsToConnectToAgent(
+ // endPoint, useInsecure, serviceName, retryInterval.toMillis(), enableConfig);
+ }
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // OcAgentTraceServiceClients.onExport(spanDataList);
+ }
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java
new file mode 100644
index 00000000..ec778ba6
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtils.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.UInt32Value;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.AttributeValue;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.ProbabilitySampler;
+import io.opencensus.proto.trace.v1.Span;
+import io.opencensus.proto.trace.v1.Span.Attributes;
+import io.opencensus.proto.trace.v1.Span.Link;
+import io.opencensus.proto.trace.v1.Span.Links;
+import io.opencensus.proto.trace.v1.Span.SpanKind;
+import io.opencensus.proto.trace.v1.Span.TimeEvent;
+import io.opencensus.proto.trace.v1.Span.TimeEvent.MessageEvent;
+import io.opencensus.proto.trace.v1.Span.Tracestate;
+import io.opencensus.proto.trace.v1.Span.Tracestate.Entry;
+import io.opencensus.proto.trace.v1.Status;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import io.opencensus.proto.trace.v1.TruncatableString;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Utilities for converting the Tracing data models in OpenCensus Java to/from OpenCensus Proto. */
+final class TraceProtoUtils {
+
+ // Constant functions for AttributeValue.
+ private static final Function<String, /*@Nullable*/ AttributeValue> stringAttributeValueFunction =
+ new Function<String, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(String stringValue) {
+ return AttributeValue.newBuilder()
+ .setStringValue(toTruncatableStringProto(stringValue))
+ .build();
+ }
+ };
+
+ private static final Function<Boolean, /*@Nullable*/ AttributeValue>
+ booleanAttributeValueFunction =
+ new Function<Boolean, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Boolean booleanValue) {
+ return AttributeValue.newBuilder().setBoolValue(booleanValue).build();
+ }
+ };
+
+ private static final Function<Long, /*@Nullable*/ AttributeValue> longAttributeValueFunction =
+ new Function<Long, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Long longValue) {
+ return AttributeValue.newBuilder().setIntValue(longValue).build();
+ }
+ };
+
+ private static final Function<Double, /*@Nullable*/ AttributeValue> doubleAttributeValueFunction =
+ new Function<Double, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Double doubleValue) {
+ return AttributeValue.newBuilder().setDoubleValue(doubleValue).build();
+ }
+ };
+
+ /**
+ * Converts {@link SpanData} to {@link Span} proto.
+ *
+ * @param spanData the {@code SpanData}.
+ * @return proto representation of {@code Span}.
+ */
+ static Span toSpanProto(SpanData spanData) {
+ SpanContext spanContext = spanData.getContext();
+ TraceId traceId = spanContext.getTraceId();
+ SpanId spanId = spanContext.getSpanId();
+ Span.Builder spanBuilder =
+ Span.newBuilder()
+ .setTraceId(toByteString(traceId.getBytes()))
+ .setSpanId(toByteString(spanId.getBytes()))
+ .setTracestate(toTracestateProto(spanContext.getTracestate()))
+ .setName(toTruncatableStringProto(spanData.getName()))
+ .setStartTime(toTimestampProto(spanData.getStartTimestamp()))
+ .setAttributes(toAttributesProto(spanData.getAttributes()))
+ .setTimeEvents(
+ toTimeEventsProto(spanData.getAnnotations(), spanData.getMessageEvents()))
+ .setLinks(toLinksProto(spanData.getLinks()));
+
+ Kind kind = spanData.getKind();
+ if (kind != null) {
+ spanBuilder.setKind(toSpanKindProto(kind));
+ }
+
+ io.opencensus.trace.Status status = spanData.getStatus();
+ if (status != null) {
+ spanBuilder.setStatus(toStatusProto(status));
+ }
+
+ Timestamp end = spanData.getEndTimestamp();
+ if (end != null) {
+ spanBuilder.setEndTime(toTimestampProto(end));
+ }
+
+ Integer childSpanCount = spanData.getChildSpanCount();
+ if (childSpanCount != null) {
+ spanBuilder.setChildSpanCount(UInt32Value.newBuilder().setValue(childSpanCount).build());
+ }
+
+ Boolean hasRemoteParent = spanData.getHasRemoteParent();
+ if (hasRemoteParent != null) {
+ spanBuilder.setSameProcessAsParentSpan(BoolValue.of(!hasRemoteParent));
+ }
+
+ SpanId parentSpanId = spanData.getParentSpanId();
+ if (parentSpanId != null && parentSpanId.isValid()) {
+ spanBuilder.setParentSpanId(toByteString(parentSpanId.getBytes()));
+ }
+
+ return spanBuilder.build();
+ }
+
+ @VisibleForTesting
+ static ByteString toByteString(byte[] bytes) {
+ return ByteString.copyFrom(bytes);
+ }
+
+ private static Tracestate toTracestateProto(io.opencensus.trace.Tracestate tracestate) {
+ return Tracestate.newBuilder().addAllEntries(toEntriesProto(tracestate.getEntries())).build();
+ }
+
+ private static List<Entry> toEntriesProto(List<io.opencensus.trace.Tracestate.Entry> entries) {
+ List<Entry> entriesProto = new ArrayList<Entry>();
+ for (io.opencensus.trace.Tracestate.Entry entry : entries) {
+ entriesProto.add(
+ Entry.newBuilder().setKey(entry.getKey()).setValue(entry.getValue()).build());
+ }
+ return entriesProto;
+ }
+
+ private static SpanKind toSpanKindProto(Kind kind) {
+ switch (kind) {
+ case CLIENT:
+ return SpanKind.CLIENT;
+ case SERVER:
+ return SpanKind.SERVER;
+ }
+ return SpanKind.UNRECOGNIZED;
+ }
+
+ private static Span.TimeEvents toTimeEventsProto(
+ TimedEvents<Annotation> annotationTimedEvents,
+ TimedEvents<io.opencensus.trace.MessageEvent> messageEventTimedEvents) {
+ Span.TimeEvents.Builder timeEventsBuilder = Span.TimeEvents.newBuilder();
+ timeEventsBuilder.setDroppedAnnotationsCount(annotationTimedEvents.getDroppedEventsCount());
+ for (TimedEvent<Annotation> annotation : annotationTimedEvents.getEvents()) {
+ timeEventsBuilder.addTimeEvent(toTimeAnnotationProto(annotation));
+ }
+ timeEventsBuilder.setDroppedMessageEventsCount(messageEventTimedEvents.getDroppedEventsCount());
+ for (TimedEvent<io.opencensus.trace.MessageEvent> networkEvent :
+ messageEventTimedEvents.getEvents()) {
+ timeEventsBuilder.addTimeEvent(toTimeMessageEventProto(networkEvent));
+ }
+ return timeEventsBuilder.build();
+ }
+
+ private static TimeEvent toTimeAnnotationProto(TimedEvent<Annotation> timedEvent) {
+ TimeEvent.Builder timeEventBuilder =
+ TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+ Annotation annotation = timedEvent.getEvent();
+ timeEventBuilder.setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(toTruncatableStringProto(annotation.getDescription()))
+ .setAttributes(toAttributesBuilderProto(annotation.getAttributes(), 0))
+ .build());
+ return timeEventBuilder.build();
+ }
+
+ private static TimeEvent toTimeMessageEventProto(
+ TimedEvent<io.opencensus.trace.MessageEvent> timedEvent) {
+ TimeEvent.Builder timeEventBuilder =
+ TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+ io.opencensus.trace.MessageEvent messageEvent = timedEvent.getEvent();
+ timeEventBuilder.setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setId(messageEvent.getMessageId())
+ .setCompressedSize(messageEvent.getCompressedMessageSize())
+ .setUncompressedSize(messageEvent.getUncompressedMessageSize())
+ .setType(toMessageEventTypeProto(messageEvent))
+ .build());
+ return timeEventBuilder.build();
+ }
+
+ private static TimeEvent.MessageEvent.Type toMessageEventTypeProto(
+ io.opencensus.trace.MessageEvent messageEvent) {
+ if (messageEvent.getType() == Type.RECEIVED) {
+ return MessageEvent.Type.RECEIVED;
+ } else {
+ return MessageEvent.Type.SENT;
+ }
+ }
+
+ private static Attributes toAttributesProto(
+ io.opencensus.trace.export.SpanData.Attributes attributes) {
+ Attributes.Builder attributesBuilder =
+ toAttributesBuilderProto(
+ attributes.getAttributeMap(), attributes.getDroppedAttributesCount());
+ return attributesBuilder.build();
+ }
+
+ private static Attributes.Builder toAttributesBuilderProto(
+ Map<String, io.opencensus.trace.AttributeValue> attributes, int droppedAttributesCount) {
+ Attributes.Builder attributesBuilder =
+ Attributes.newBuilder().setDroppedAttributesCount(droppedAttributesCount);
+ for (Map.Entry<String, io.opencensus.trace.AttributeValue> label : attributes.entrySet()) {
+ AttributeValue value = toAttributeValueProto(label.getValue());
+ if (value != null) {
+ attributesBuilder.putAttributeMap(label.getKey(), value);
+ }
+ }
+ return attributesBuilder;
+ }
+
+ @javax.annotation.Nullable
+ private static AttributeValue toAttributeValueProto(
+ io.opencensus.trace.AttributeValue attributeValue) {
+ return attributeValue.match(
+ stringAttributeValueFunction,
+ booleanAttributeValueFunction,
+ longAttributeValueFunction,
+ doubleAttributeValueFunction,
+ Functions.</*@Nullable*/ AttributeValue>returnNull());
+ }
+
+ private static Status toStatusProto(io.opencensus.trace.Status status) {
+ Status.Builder statusBuilder = Status.newBuilder().setCode(status.getCanonicalCode().value());
+ if (status.getDescription() != null) {
+ statusBuilder.setMessage(status.getDescription());
+ }
+ return statusBuilder.build();
+ }
+
+ @VisibleForTesting
+ static TruncatableString toTruncatableStringProto(String string) {
+ return TruncatableString.newBuilder().setValue(string).setTruncatedByteCount(0).build();
+ }
+
+ static com.google.protobuf.Timestamp toTimestampProto(Timestamp timestamp) {
+ return com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(timestamp.getSeconds())
+ .setNanos(timestamp.getNanos())
+ .build();
+ }
+
+ private static Link.Type toLinkTypeProto(io.opencensus.trace.Link.Type type) {
+ if (type == io.opencensus.trace.Link.Type.PARENT_LINKED_SPAN) {
+ return Link.Type.PARENT_LINKED_SPAN;
+ } else {
+ return Link.Type.CHILD_LINKED_SPAN;
+ }
+ }
+
+ private static Link toLinkProto(io.opencensus.trace.Link link) {
+ return Link.newBuilder()
+ .setTraceId(toByteString(link.getTraceId().getBytes()))
+ .setSpanId(toByteString(link.getSpanId().getBytes()))
+ .setType(toLinkTypeProto(link.getType()))
+ .setAttributes(toAttributesBuilderProto(link.getAttributes(), 0))
+ .build();
+ }
+
+ private static Links toLinksProto(io.opencensus.trace.export.SpanData.Links links) {
+ final Links.Builder linksBuilder =
+ Links.newBuilder().setDroppedLinksCount(links.getDroppedLinksCount());
+ for (io.opencensus.trace.Link link : links.getLinks()) {
+ linksBuilder.addLink(toLinkProto(link));
+ }
+ return linksBuilder.build();
+ }
+
+ /**
+ * Converts {@link TraceParams} to {@link TraceConfig}.
+ *
+ * @param traceParams the {@code TraceParams}.
+ * @return {@code TraceConfig}.
+ */
+ static TraceConfig toTraceConfigProto(TraceParams traceParams) {
+ TraceConfig.Builder traceConfigProtoBuilder = TraceConfig.newBuilder();
+ Sampler librarySampler = traceParams.getSampler();
+
+ if (Samplers.alwaysSample().equals(librarySampler)) {
+ traceConfigProtoBuilder.setConstantSampler(
+ ConstantSampler.newBuilder().setDecision(true).build());
+ } else if (Samplers.neverSample().equals(librarySampler)) {
+ traceConfigProtoBuilder.setConstantSampler(
+ ConstantSampler.newBuilder().setDecision(false).build());
+ } else {
+ // TODO: consider exposing the sampling probability of ProbabilitySampler.
+ double samplingProbability = parseSamplingProbability(librarySampler);
+ traceConfigProtoBuilder.setProbabilitySampler(
+ ProbabilitySampler.newBuilder().setSamplingProbability(samplingProbability).build());
+ } // TODO: add support for RateLimitingSampler.
+
+ return traceConfigProtoBuilder.build();
+ }
+
+ private static double parseSamplingProbability(Sampler sampler) {
+ String description = sampler.getDescription();
+ // description follows format "ProbabilitySampler{%.6f}", samplingProbability.
+ int leftParenIndex = description.indexOf("{");
+ int rightParenIndex = description.indexOf("}");
+ return Double.parseDouble(description.substring(leftParenIndex + 1, rightParenIndex));
+ }
+
+ /**
+ * Converts {@link TraceConfig} to {@link TraceParams}.
+ *
+ * @param traceConfigProto {@code TraceConfig}.
+ * @param currentTraceParams current {@code TraceParams}.
+ * @return updated {@code TraceParams}.
+ * @since 0.17
+ */
+ static TraceParams fromTraceConfigProto(
+ TraceConfig traceConfigProto, TraceParams currentTraceParams) {
+ TraceParams.Builder builder = currentTraceParams.toBuilder();
+ if (traceConfigProto.hasConstantSampler()) {
+ ConstantSampler constantSampler = traceConfigProto.getConstantSampler();
+ if (Boolean.TRUE.equals(constantSampler.getDecision())) {
+ builder.setSampler(Samplers.alwaysSample());
+ } else {
+ builder.setSampler(Samplers.neverSample());
+ }
+ } else if (traceConfigProto.hasProbabilitySampler()) {
+ builder.setSampler(
+ Samplers.probabilitySampler(
+ traceConfigProto.getProbabilitySampler().getSamplingProbability()));
+ } // TODO: add support for RateLimitingSampler.
+ return builder.build();
+ }
+
+ // Creates a TraceConfig proto message with current TraceParams.
+ static TraceConfig getCurrentTraceConfig(io.opencensus.trace.config.TraceConfig traceConfig) {
+ TraceParams traceParams = traceConfig.getActiveTraceParams();
+ return toTraceConfigProto(traceParams);
+ }
+
+ // Creates an updated TraceParams with the given UpdatedLibraryConfig message and current
+ // TraceParams, then applies the updated TraceParams.
+ static TraceParams getUpdatedTraceParams(
+ UpdatedLibraryConfig config, io.opencensus.trace.config.TraceConfig traceConfig) {
+ TraceParams currentParams = traceConfig.getActiveTraceParams();
+ TraceConfig traceConfigProto = config.getConfig();
+ return fromTraceConfigProto(traceConfigProto, currentParams);
+ }
+
+ private TraceProtoUtils() {}
+}
diff --git a/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java
new file mode 100644
index 00000000..d01dd7eb
--- /dev/null
+++ b/exporters/trace/ocagent/src/main/java/io/opencensus/exporter/trace/ocagent/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018, 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.
+ */
+
+/**
+ * This package contains the Java implementation of the OpenCensus Agent (OC-Agent) Trace Exporter.
+ *
+ * <p>WARNING: Currently all the public classes under this package are marked as {@link
+ * io.opencensus.common.ExperimentalApi}. The classes and APIs under {@link
+ * io.opencensus.exporter.trace.ocagent} are likely to get backwards-incompatible updates in the
+ * future. DO NOT USE except for experimental purposes.
+ *
+ * <p>See more details on
+ * https://github.com/census-instrumentation/opencensus-proto/tree/master/src/opencensus/proto/agent.
+ */
+@io.opencensus.common.ExperimentalApi
+package io.opencensus.exporter.trace.ocagent;
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java
new file mode 100644
index 00000000..fbdb35e3
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImpl.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+import io.grpc.netty.NettyServerBuilder;
+import io.grpc.stub.StreamObserver;
+import io.opencensus.proto.agent.trace.v1.CurrentLibraryConfig;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceRequest;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceResponse;
+import io.opencensus.proto.agent.trace.v1.TraceServiceGrpc;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+
+/** Fake implementation of {@link TraceServiceGrpc}. */
+final class FakeOcAgentTraceServiceGrpcImpl extends TraceServiceGrpc.TraceServiceImplBase {
+
+ private static final Logger logger =
+ Logger.getLogger(FakeOcAgentTraceServiceGrpcImpl.class.getName());
+
+ // Default updatedLibraryConfig uses an always sampler.
+ private UpdatedLibraryConfig updatedLibraryConfig =
+ UpdatedLibraryConfig.newBuilder()
+ .setConfig(
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+ .build())
+ .build();
+
+ private final List<CurrentLibraryConfig> currentLibraryConfigs = new ArrayList<>();
+ private final List<ExportTraceServiceRequest> exportTraceServiceRequests = new ArrayList<>();
+
+ private final AtomicReference<StreamObserver<UpdatedLibraryConfig>> updatedConfigObserverRef =
+ new AtomicReference<>();
+
+ private final StreamObserver<CurrentLibraryConfig> currentConfigObserver =
+ new StreamObserver<CurrentLibraryConfig>() {
+ @Override
+ public void onNext(CurrentLibraryConfig value) {
+ currentLibraryConfigs.add(value);
+ @Nullable
+ StreamObserver<UpdatedLibraryConfig> updatedConfigObserver =
+ updatedConfigObserverRef.get();
+ if (updatedConfigObserver != null) {
+ updatedConfigObserver.onNext(updatedLibraryConfig);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ logger.warning("Exception thrown for config stream: " + t);
+ }
+
+ @Override
+ public void onCompleted() {}
+ };
+
+ private final StreamObserver<ExportTraceServiceRequest> exportRequestObserver =
+ new StreamObserver<ExportTraceServiceRequest>() {
+ @Override
+ public void onNext(ExportTraceServiceRequest value) {
+ exportTraceServiceRequests.add(value);
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ logger.warning("Exception thrown for export stream: " + t);
+ }
+
+ @Override
+ public void onCompleted() {}
+ };
+
+ @Override
+ public StreamObserver<CurrentLibraryConfig> config(
+ StreamObserver<UpdatedLibraryConfig> updatedLibraryConfigStreamObserver) {
+ updatedConfigObserverRef.set(updatedLibraryConfigStreamObserver);
+ return currentConfigObserver;
+ }
+
+ @Override
+ public StreamObserver<ExportTraceServiceRequest> export(
+ StreamObserver<ExportTraceServiceResponse> exportTraceServiceResponseStreamObserver) {
+ return exportRequestObserver;
+ }
+
+ // Returns the stored CurrentLibraryConfigs.
+ List<CurrentLibraryConfig> getCurrentLibraryConfigs() {
+ return Collections.unmodifiableList(currentLibraryConfigs);
+ }
+
+ // Returns the stored ExportTraceServiceRequests.
+ List<ExportTraceServiceRequest> getExportTraceServiceRequests() {
+ return Collections.unmodifiableList(exportTraceServiceRequests);
+ }
+
+ // Sets the UpdatedLibraryConfig that will be passed to client.
+ void setUpdatedLibraryConfig(UpdatedLibraryConfig updatedLibraryConfig) {
+ this.updatedLibraryConfig = updatedLibraryConfig;
+ }
+
+ // Gets the UpdatedLibraryConfig that will be passed to client.
+ UpdatedLibraryConfig getUpdatedLibraryConfig() {
+ return updatedLibraryConfig;
+ }
+
+ static void startServer(String endPoint) throws IOException {
+ ServerBuilder<?> builder = NettyServerBuilder.forAddress(parseEndpoint(endPoint));
+ Executor executor = MoreExecutors.directExecutor();
+ builder.executor(executor);
+ final Server server = builder.addService(new FakeOcAgentTraceServiceGrpcImpl()).build();
+ server.start();
+ logger.info("Server started at " + endPoint);
+
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread() {
+ @Override
+ public void run() {
+ server.shutdown();
+ }
+ });
+
+ try {
+ server.awaitTermination();
+ } catch (InterruptedException e) {
+ logger.warning("Thread interrupted: " + e.getMessage());
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private static InetSocketAddress parseEndpoint(String endPoint) {
+ try {
+ int colonIndex = endPoint.indexOf(":");
+ String host = endPoint.substring(0, colonIndex);
+ int port = Integer.parseInt(endPoint.substring(colonIndex + 1));
+ return new InetSocketAddress(host, port);
+ } catch (RuntimeException e) {
+ logger.warning("Unexpected format of end point: " + endPoint + ", use default end point.");
+ return new InetSocketAddress("localhost", 55678);
+ }
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java
new file mode 100644
index 00000000..f619021b
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/FakeOcAgentTraceServiceGrpcImplTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.grpc.stub.StreamObserver;
+import io.opencensus.proto.agent.trace.v1.CurrentLibraryConfig;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceRequest;
+import io.opencensus.proto.agent.trace.v1.ExportTraceServiceResponse;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link FakeOcAgentTraceServiceGrpcImpl}. */
+@RunWith(JUnit4.class)
+public class FakeOcAgentTraceServiceGrpcImplTest {
+
+ private final List<UpdatedLibraryConfig> updatedLibraryConfigs = new ArrayList<>();
+
+ private final StreamObserver<UpdatedLibraryConfig> updatedConfigObserver =
+ new StreamObserver<UpdatedLibraryConfig>() {
+
+ @Override
+ public void onNext(UpdatedLibraryConfig value) {
+ updatedLibraryConfigs.add(value);
+ }
+
+ @Override
+ public void onError(Throwable t) {}
+
+ @Override
+ public void onCompleted() {}
+ };
+
+ private final StreamObserver<ExportTraceServiceResponse> exportResponseObserver =
+ new StreamObserver<ExportTraceServiceResponse>() {
+ @Override
+ public void onNext(ExportTraceServiceResponse value) {}
+
+ @Override
+ public void onError(Throwable t) {}
+
+ @Override
+ public void onCompleted() {}
+ };
+
+ private static final UpdatedLibraryConfig neverSampledLibraryConfig =
+ UpdatedLibraryConfig.newBuilder()
+ .setConfig(
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+ .build())
+ .build();
+
+ @Test
+ public void export() {
+ FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+ StreamObserver<ExportTraceServiceRequest> exportRequestObserver =
+ traceServiceGrpc.export(exportResponseObserver);
+ ExportTraceServiceRequest request = ExportTraceServiceRequest.getDefaultInstance();
+ exportRequestObserver.onNext(request);
+ assertThat(traceServiceGrpc.getExportTraceServiceRequests()).containsExactly(request);
+ }
+
+ @Test
+ public void config() {
+ FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+ StreamObserver<CurrentLibraryConfig> currentConfigObsever =
+ traceServiceGrpc.config(updatedConfigObserver);
+ CurrentLibraryConfig currentLibraryConfig = CurrentLibraryConfig.getDefaultInstance();
+ currentConfigObsever.onNext(currentLibraryConfig);
+ assertThat(traceServiceGrpc.getCurrentLibraryConfigs()).containsExactly(currentLibraryConfig);
+ assertThat(updatedLibraryConfigs).containsExactly(traceServiceGrpc.getUpdatedLibraryConfig());
+ updatedLibraryConfigs.clear();
+ }
+
+ @Test
+ public void config_WithNeverSampler() {
+ FakeOcAgentTraceServiceGrpcImpl traceServiceGrpc = new FakeOcAgentTraceServiceGrpcImpl();
+ traceServiceGrpc.setUpdatedLibraryConfig(neverSampledLibraryConfig);
+ StreamObserver<CurrentLibraryConfig> currentConfigObsever =
+ traceServiceGrpc.config(updatedConfigObserver);
+ CurrentLibraryConfig currentLibraryConfig = CurrentLibraryConfig.getDefaultInstance();
+ currentConfigObsever.onNext(currentLibraryConfig);
+ assertThat(traceServiceGrpc.getCurrentLibraryConfigs()).containsExactly(currentLibraryConfig);
+ assertThat(updatedLibraryConfigs).containsExactly(neverSampledLibraryConfig);
+ updatedLibraryConfigs.clear();
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java
new file mode 100644
index 00000000..813066bc
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentNodeUtilsTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.OC_AGENT_EXPORTER_VERSION;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.RESOURCE_LABEL_ATTRIBUTE_KEY;
+import static io.opencensus.exporter.trace.ocagent.OcAgentNodeUtils.RESOURCE_TYPE_ATTRIBUTE_KEY;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.proto.agent.common.v1.LibraryInfo;
+import io.opencensus.proto.agent.common.v1.LibraryInfo.Language;
+import io.opencensus.proto.agent.common.v1.ProcessIdentifier;
+import io.opencensus.proto.agent.common.v1.ServiceInfo;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link OcAgentNodeUtils}. */
+@RunWith(JUnit4.class)
+public class OcAgentNodeUtilsTest {
+
+ private static final AwsEc2InstanceMonitoredResource AWS_RESOURCE =
+ AwsEc2InstanceMonitoredResource.create("account1", "instance1", "us-east-2");
+ private static final GcpGceInstanceMonitoredResource GCE_RESOURCE =
+ GcpGceInstanceMonitoredResource.create("account2", "instance2", "us-west2");
+ private static final GcpGkeContainerMonitoredResource GKE_RESOURCE =
+ GcpGkeContainerMonitoredResource.create(
+ "account3", "cluster", "container", "", "instance3", "", "us-west4");
+
+ @Test
+ public void testConstants() {
+ assertThat(OC_AGENT_EXPORTER_VERSION).isEqualTo("0.17.0-SNAPSHOT");
+ assertThat(RESOURCE_TYPE_ATTRIBUTE_KEY).isEqualTo("OPENCENSUS_SOURCE_TYPE");
+ assertThat(RESOURCE_LABEL_ATTRIBUTE_KEY).isEqualTo("OPENCENSUS_SOURCE_LABELS");
+ }
+
+ @Test
+ public void getProcessIdentifier() {
+ String jvmName = "54321@my.org";
+ Timestamp timestamp = Timestamp.create(10, 20);
+ ProcessIdentifier processIdentifier = OcAgentNodeUtils.getProcessIdentifier(jvmName, timestamp);
+ assertThat(processIdentifier.getHostName()).isEqualTo("my.org");
+ assertThat(processIdentifier.getPid()).isEqualTo(54321);
+ assertThat(processIdentifier.getStartTimestamp())
+ .isEqualTo(com.google.protobuf.Timestamp.newBuilder().setSeconds(10).setNanos(20).build());
+ }
+
+ @Test
+ public void getLibraryInfo() {
+ String currentOcJavaVersion = "0.16.0";
+ LibraryInfo libraryInfo = OcAgentNodeUtils.getLibraryInfo(currentOcJavaVersion);
+ assertThat(libraryInfo.getLanguage()).isEqualTo(Language.JAVA);
+ assertThat(libraryInfo.getCoreLibraryVersion()).isEqualTo(currentOcJavaVersion);
+ assertThat(libraryInfo.getExporterVersion()).isEqualTo(OC_AGENT_EXPORTER_VERSION);
+ }
+
+ @Test
+ public void getServiceInfo() {
+ String serviceName = "my-service";
+ ServiceInfo serviceInfo = OcAgentNodeUtils.getServiceInfo(serviceName);
+ assertThat(serviceInfo.getName()).isEqualTo(serviceName);
+ }
+
+ @Test
+ public void getAttributeMap_Null() {
+ Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(null);
+ assertThat(attributeMap).isEmpty();
+ }
+
+ @Test
+ public void getAttributeMap_AwsEc2Resource() {
+ Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(AWS_RESOURCE);
+ assertThat(attributeMap)
+ .containsExactly(
+ RESOURCE_TYPE_ATTRIBUTE_KEY,
+ "AWS_EC2_INSTANCE",
+ RESOURCE_LABEL_ATTRIBUTE_KEY,
+ "aws_account=account1,instance_id=instance1,region=us-east-2");
+ }
+
+ @Test
+ public void getAttributeMap_GceResource() {
+ Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(GCE_RESOURCE);
+ assertThat(attributeMap)
+ .containsExactly(
+ RESOURCE_TYPE_ATTRIBUTE_KEY,
+ "GCP_GCE_INSTANCE",
+ RESOURCE_LABEL_ATTRIBUTE_KEY,
+ "gcp_account=account2,instance_id=instance2,zone=us-west2");
+ }
+
+ @Test
+ public void getAttributeMap_GkeResource() {
+ Map<String, String> attributeMap = OcAgentNodeUtils.getAttributeMap(GKE_RESOURCE);
+ assertThat(attributeMap)
+ .containsExactly(
+ RESOURCE_TYPE_ATTRIBUTE_KEY,
+ "GCP_GKE_CONTAINER",
+ RESOURCE_LABEL_ATTRIBUTE_KEY,
+ "gcp_account=account3,instance_id=instance3,location=us-west4,"
+ + "cluster_name=cluster,container_name=container");
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java
new file mode 100644
index 00000000..81bc5c60
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterConfigurationTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link OcAgentTraceExporterConfiguration}. */
+@RunWith(JUnit4.class)
+public class OcAgentTraceExporterConfigurationTest {
+
+ @Test
+ public void defaultConfiguration() {
+ OcAgentTraceExporterConfiguration configuration =
+ OcAgentTraceExporterConfiguration.builder().build();
+ assertThat(configuration.getEndPoint()).isNull();
+ assertThat(configuration.getServiceName()).isNull();
+ assertThat(configuration.getUseInsecure()).isNull();
+ assertThat(configuration.getRetryInterval()).isNull();
+ assertThat(configuration.getEnableConfig()).isTrue();
+ }
+
+ @Test
+ public void setAndGet() {
+ Duration oneMinute = Duration.create(60, 0);
+ OcAgentTraceExporterConfiguration configuration =
+ OcAgentTraceExporterConfiguration.builder()
+ .setEndPoint("192.168.0.1:50051")
+ .setServiceName("service")
+ .setUseInsecure(true)
+ .setRetryInterval(oneMinute)
+ .setEnableConfig(false)
+ .build();
+ assertThat(configuration.getEndPoint()).isEqualTo("192.168.0.1:50051");
+ assertThat(configuration.getServiceName()).isEqualTo("service");
+ assertThat(configuration.getUseInsecure()).isTrue();
+ assertThat(configuration.getRetryInterval()).isEqualTo(oneMinute);
+ assertThat(configuration.getEnableConfig()).isFalse();
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java
new file mode 100644
index 00000000..c58acdb1
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/OcAgentTraceExporterTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link OcAgentTraceExporter}. */
+@RunWith(JUnit4.class)
+public class OcAgentTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+ @Mock private Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterOcAgentTraceExporter() {
+ OcAgentTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.ocagent.OcAgentTraceExporter"),
+ any(OcAgentTraceExporterHandler.class));
+ OcAgentTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.ocagent.OcAgentTraceExporter"));
+ }
+}
diff --git a/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java
new file mode 100644
index 00000000..74c7c29e
--- /dev/null
+++ b/exporters/trace/ocagent/src/test/java/io/opencensus/exporter/trace/ocagent/TraceProtoUtilsTest.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2018, 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.exporter.trace.ocagent;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.exporter.trace.ocagent.TraceProtoUtils.toByteString;
+import static io.opencensus.exporter.trace.ocagent.TraceProtoUtils.toTruncatableStringProto;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.BoolValue;
+import com.google.protobuf.UInt32Value;
+import io.opencensus.common.Timestamp;
+import io.opencensus.proto.agent.trace.v1.UpdatedLibraryConfig;
+import io.opencensus.proto.trace.v1.AttributeValue;
+import io.opencensus.proto.trace.v1.ConstantSampler;
+import io.opencensus.proto.trace.v1.ProbabilitySampler;
+import io.opencensus.proto.trace.v1.Span;
+import io.opencensus.proto.trace.v1.Span.SpanKind;
+import io.opencensus.proto.trace.v1.Span.TimeEvent;
+import io.opencensus.proto.trace.v1.Span.TimeEvent.MessageEvent;
+import io.opencensus.proto.trace.v1.TraceConfig;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for {@link TraceProtoUtils}. */
+@RunWith(JUnit4.class)
+public class TraceProtoUtilsTest {
+
+ @Mock private io.opencensus.trace.config.TraceConfig mockTraceConfig;
+
+ private static final TraceParams DEFAULT_PARAMS = TraceParams.DEFAULT;
+
+ private static final Timestamp startTimestamp = Timestamp.create(123, 456);
+ private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457);
+ private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458);
+ private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459);
+ private static final Timestamp endTimestamp = Timestamp.create(123, 460);
+
+ private static final String TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
+ private static final String SPAN_ID = "24aa0b2d371f48c9";
+ private static final String PARENT_SPAN_ID = "71da8d631536f5f1";
+ private static final String SPAN_NAME = "MySpanName";
+ private static final String ANNOTATION_TEXT = "MyAnnotationText";
+ private static final String ATTRIBUTE_KEY_1 = "MyAttributeKey1";
+ private static final String ATTRIBUTE_KEY_2 = "MyAttributeKey2";
+
+ private static final String FIRST_KEY = "key_1";
+ private static final String SECOND_KEY = "key_2";
+ private static final String FIRST_VALUE = "value.1";
+ private static final String SECOND_VALUE = "value.2";
+ private static final Tracestate multiValueTracestate =
+ Tracestate.builder().set(FIRST_KEY, FIRST_VALUE).set(SECOND_KEY, SECOND_VALUE).build();
+
+ private static final int DROPPED_ATTRIBUTES_COUNT = 1;
+ private static final int DROPPED_ANNOTATIONS_COUNT = 2;
+ private static final int DROPPED_NETWORKEVENTS_COUNT = 3;
+ private static final int DROPPED_LINKS_COUNT = 4;
+ private static final int CHILD_SPAN_COUNT = 13;
+
+ private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT);
+ private static final io.opencensus.trace.MessageEvent recvMessageEvent =
+ io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.RECEIVED, 1)
+ .build();
+ private static final io.opencensus.trace.MessageEvent sentMessageEvent =
+ io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.SENT, 1)
+ .build();
+ private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow");
+ private static final SpanId parentSpanId = SpanId.fromLowerBase16(PARENT_SPAN_ID);
+ private static final SpanId spanId = SpanId.fromLowerBase16(SPAN_ID);
+ private static final TraceId traceId = TraceId.fromLowerBase16(TRACE_ID);
+ private static final TraceOptions traceOptions = TraceOptions.DEFAULT;
+ private static final SpanContext spanContext =
+ SpanContext.create(traceId, spanId, traceOptions, multiValueTracestate);
+
+ private static final List<TimedEvent<Annotation>> annotationsList =
+ ImmutableList.of(
+ SpanData.TimedEvent.create(eventTimestamp1, annotation),
+ SpanData.TimedEvent.create(eventTimestamp3, annotation));
+ private static final List<TimedEvent<io.opencensus.trace.MessageEvent>> networkEventsList =
+ ImmutableList.of(
+ SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent),
+ SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent));
+ private static final List<Link> linksList =
+ ImmutableList.of(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN));
+
+ private static final SpanData.Attributes attributes =
+ SpanData.Attributes.create(
+ ImmutableMap.of(
+ ATTRIBUTE_KEY_1,
+ io.opencensus.trace.AttributeValue.longAttributeValue(10L),
+ ATTRIBUTE_KEY_2,
+ io.opencensus.trace.AttributeValue.booleanAttributeValue(true)),
+ DROPPED_ATTRIBUTES_COUNT);
+ private static final TimedEvents<Annotation> annotations =
+ TimedEvents.create(annotationsList, DROPPED_ANNOTATIONS_COUNT);
+ private static final TimedEvents<io.opencensus.trace.MessageEvent> messageEvents =
+ TimedEvents.create(networkEventsList, DROPPED_NETWORKEVENTS_COUNT);
+ private static final SpanData.Links links = SpanData.Links.create(linksList, DROPPED_LINKS_COUNT);
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Mockito.when(mockTraceConfig.getActiveTraceParams()).thenReturn(DEFAULT_PARAMS);
+ Mockito.doNothing()
+ .when(mockTraceConfig)
+ .updateActiveTraceParams(Mockito.any(TraceParams.class));
+ }
+
+ @Test
+ public void toSpanProto() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ false,
+ SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ TimeEvent annotationTimeEvent1 =
+ TimeEvent.newBuilder()
+ .setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(toTruncatableStringProto(ANNOTATION_TEXT))
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp1.getSeconds())
+ .setNanos(eventTimestamp1.getNanos())
+ .build())
+ .build();
+ TimeEvent annotationTimeEvent2 =
+ TimeEvent.newBuilder()
+ .setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(toTruncatableStringProto(ANNOTATION_TEXT))
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp3.getSeconds())
+ .setNanos(eventTimestamp3.getNanos())
+ .build())
+ .build();
+
+ TimeEvent sentTimeEvent =
+ TimeEvent.newBuilder()
+ .setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setType(MessageEvent.Type.SENT)
+ .setId(sentMessageEvent.getMessageId()))
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp2.getSeconds())
+ .setNanos(eventTimestamp2.getNanos())
+ .build())
+ .build();
+ TimeEvent recvTimeEvent =
+ TimeEvent.newBuilder()
+ .setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setType(MessageEvent.Type.RECEIVED)
+ .setId(recvMessageEvent.getMessageId()))
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp1.getSeconds())
+ .setNanos(eventTimestamp1.getNanos())
+ .build())
+ .build();
+
+ Span.Links spanLinks =
+ Span.Links.newBuilder()
+ .setDroppedLinksCount(DROPPED_LINKS_COUNT)
+ .addLink(
+ Span.Link.newBuilder()
+ .setType(Span.Link.Type.CHILD_LINKED_SPAN)
+ .setTraceId(toByteString(traceId.getBytes()))
+ .setSpanId(toByteString(spanId.getBytes()))
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .build();
+
+ io.opencensus.proto.trace.v1.Status spanStatus =
+ io.opencensus.proto.trace.v1.Status.newBuilder()
+ .setCode(com.google.rpc.Code.DEADLINE_EXCEEDED.getNumber())
+ .setMessage("TooSlow")
+ .build();
+
+ com.google.protobuf.Timestamp startTime =
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(startTimestamp.getSeconds())
+ .setNanos(startTimestamp.getNanos())
+ .build();
+ com.google.protobuf.Timestamp endTime =
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(endTimestamp.getSeconds())
+ .setNanos(endTimestamp.getNanos())
+ .build();
+
+ Span span = TraceProtoUtils.toSpanProto(spanData);
+ assertThat(span.getName()).isEqualTo(toTruncatableStringProto(SPAN_NAME));
+ assertThat(span.getTraceId()).isEqualTo(toByteString(traceId.getBytes()));
+ assertThat(span.getSpanId()).isEqualTo(toByteString(spanId.getBytes()));
+ assertThat(span.getParentSpanId()).isEqualTo(toByteString(parentSpanId.getBytes()));
+ assertThat(span.getStartTime()).isEqualTo(startTime);
+ assertThat(span.getEndTime()).isEqualTo(endTime);
+ assertThat(span.getKind()).isEqualTo(SpanKind.CLIENT);
+ assertThat(span.getAttributes().getDroppedAttributesCount())
+ .isEqualTo(DROPPED_ATTRIBUTES_COUNT);
+ // The generated attributes map contains more values (e.g. agent). We only test what we added.
+ assertThat(span.getAttributes().getAttributeMapMap())
+ .containsEntry(ATTRIBUTE_KEY_1, AttributeValue.newBuilder().setIntValue(10L).build());
+ assertThat(span.getAttributes().getAttributeMapMap())
+ .containsEntry(ATTRIBUTE_KEY_2, AttributeValue.newBuilder().setBoolValue(true).build());
+ assertThat(span.getTimeEvents().getDroppedMessageEventsCount())
+ .isEqualTo(DROPPED_NETWORKEVENTS_COUNT);
+ assertThat(span.getTimeEvents().getDroppedAnnotationsCount())
+ .isEqualTo(DROPPED_ANNOTATIONS_COUNT);
+ assertThat(span.getTimeEvents().getTimeEventList())
+ .containsAllOf(annotationTimeEvent1, annotationTimeEvent2, sentTimeEvent, recvTimeEvent);
+ assertThat(span.getLinks()).isEqualTo(spanLinks);
+ assertThat(span.getStatus()).isEqualTo(spanStatus);
+ assertThat(span.getSameProcessAsParentSpan()).isEqualTo(BoolValue.of(true));
+ assertThat(span.getChildSpanCount())
+ .isEqualTo(UInt32Value.newBuilder().setValue(CHILD_SPAN_COUNT).build());
+ }
+
+ @Test
+ public void toTraceConfigProto_AlwaysSampler() {
+ assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.alwaysSample())))
+ .isEqualTo(
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+ .build());
+ }
+
+ @Test
+ public void toTraceConfigProto_NeverSampler() {
+ assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.neverSample())))
+ .isEqualTo(
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+ .build());
+ }
+
+ @Test
+ public void toTraceConfigProto_ProbabilitySampler() {
+ assertThat(TraceProtoUtils.toTraceConfigProto(getTraceParams(Samplers.probabilitySampler(0.5))))
+ .isEqualTo(
+ TraceConfig.newBuilder()
+ .setProbabilitySampler(
+ ProbabilitySampler.newBuilder().setSamplingProbability(0.5).build())
+ .build());
+ }
+
+ @Test
+ public void fromTraceConfigProto_AlwaysSampler() {
+ TraceConfig traceConfig =
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(true).build())
+ .build();
+ assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+ .isEqualTo(Samplers.alwaysSample());
+ }
+
+ @Test
+ public void fromTraceConfigProto_NeverSampler() {
+ TraceConfig traceConfig =
+ TraceConfig.newBuilder()
+ .setConstantSampler(ConstantSampler.newBuilder().setDecision(false).build())
+ .build();
+ assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+ .isEqualTo(Samplers.neverSample());
+ }
+
+ @Test
+ public void fromTraceConfigProto_ProbabilitySampler() {
+ TraceConfig traceConfig =
+ TraceConfig.newBuilder()
+ .setProbabilitySampler(
+ ProbabilitySampler.newBuilder().setSamplingProbability(0.01).build())
+ .build();
+ assertThat(TraceProtoUtils.fromTraceConfigProto(traceConfig, DEFAULT_PARAMS).getSampler())
+ .isEqualTo(Samplers.probabilitySampler(0.01));
+ }
+
+ @Test
+ public void getCurrentTraceConfig() {
+ TraceConfig configProto = TraceProtoUtils.toTraceConfigProto(DEFAULT_PARAMS);
+ assertThat(TraceProtoUtils.getCurrentTraceConfig(mockTraceConfig)).isEqualTo(configProto);
+ Mockito.verify(mockTraceConfig, Mockito.times(1)).getActiveTraceParams();
+ }
+
+ @Test
+ public void applyUpdatedConfig() {
+ TraceConfig configProto =
+ TraceConfig.newBuilder()
+ .setProbabilitySampler(
+ ProbabilitySampler.newBuilder().setSamplingProbability(0.01).build())
+ .build();
+ UpdatedLibraryConfig updatedLibraryConfig =
+ UpdatedLibraryConfig.newBuilder().setConfig(configProto).build();
+ TraceParams traceParams =
+ TraceProtoUtils.getUpdatedTraceParams(updatedLibraryConfig, mockTraceConfig);
+ TraceParams expectedParams =
+ DEFAULT_PARAMS.toBuilder().setSampler(Samplers.probabilitySampler(0.01)).build();
+ Mockito.verify(mockTraceConfig, Mockito.times(1)).getActiveTraceParams();
+ assertThat(traceParams).isEqualTo(expectedParams);
+ }
+
+ private static TraceParams getTraceParams(Sampler sampler) {
+ return DEFAULT_PARAMS.toBuilder().setSampler(sampler).build();
+ }
+}
diff --git a/exporters/trace/stackdriver/README.md b/exporters/trace/stackdriver/README.md
new file mode 100644
index 00000000..9186a47c
--- /dev/null
+++ b/exporters/trace/stackdriver/README.md
@@ -0,0 +1,127 @@
+# OpenCensus Stackdriver Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Stackdriver Trace Exporter* is a trace exporter that exports data to
+Stackdriver Trace. [Stackdriver Trace][stackdriver-trace] is a distributed
+tracing system that collects latency data from your applications and displays it in the Google
+Cloud Platform Console. You can track how requests propagate through your application and receive
+detailed near real-time performance insights.
+
+## Quickstart
+
+### Prerequisites
+
+To use this exporter, you must have an application that you'd like to trace. The app can be on
+Google Cloud Platform, on-premise, or another cloud platform.
+
+In order to be able to push your traces to [Stackdriver Trace][stackdriver-trace], you must:
+
+1. [Create a Cloud project](https://support.google.com/cloud/answer/6251787?hl=en).
+2. [Enable billing](https://support.google.com/cloud/answer/6288653#new-billing).
+3. [Enable the Stackdriver Trace API](https://console.cloud.google.com/apis/api/cloudtrace.googleapis.com/overview).
+
+These steps enable the API but don't require that your app is hosted on Google Cloud Platform.
+
+### Hello "Stackdriver Trace"
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-stackdriver</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-stackdriver:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This uses the default configuration for authentication and project ID.
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder().build());
+ // ...
+ }
+}
+```
+
+#### Authentication
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the authentication see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication).
+
+If you prefer to manually set the credentials use:
+```
+StackdriverTraceExporter.createAndRegisterWithCredentialsAndProjectId(
+ new GoogleCredentials(new AccessToken(accessToken, expirationTime)),
+ "MyStackdriverProjectId");
+```
+
+#### Specifying a Project ID
+
+This exporter uses [google-cloud-java](https://github.com/GoogleCloudPlatform/google-cloud-java),
+for details about how to configure the project ID see [here](https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id).
+
+If you prefer to manually set the project ID use:
+```
+StackdriverTraceExporter.createAndRegisterWithProjectId("MyStackdriverProjectId");
+```
+
+#### Enable Stackdriver Trace API access scope on Google Cloud Platform
+If your Stackdriver Trace Exporter is running on Kubernetes Engine or Compute Engine,
+you might need additional setup to explicitly enable the ```trace.append``` Stackdriver
+Trace API access scope. To do that, please follow the instructions for
+[GKE](https://cloud.google.com/trace/docs/setup/java#kubernetes_engine) or
+[GCE](https://cloud.google.com/trace/docs/setup/java#compute_engine).
+
+#### Java Versions
+
+Java 7 or above is required for using this exporter.
+
+## FAQ
+### Why do I not see some trace events in Stackdriver?
+In all the versions before '0.9.1' the Stackdriver Trace exporter was implemented using the [v1
+API][stackdriver-v1-api-url] which is not fully compatible with the OpenCensus data model. Trace
+events like Annotations and NetworkEvents will be dropped.
+
+### Why do I get a "StatusRuntimeException: NOT_FOUND: Requested entity was not found"?
+One of the possible reasons is you are using a project id with bad format for the exporter.
+Please double check the project id associated with the Stackdriver Trace exporter first.
+Stackdriver Trace backend will not do any sanitization or trimming on the incoming project id.
+Project id with leading or trailing spaces will be treated as a separate non-existing project
+(e.g "project-id" vs "project-id "), and will cause a NOT_FOUND exception.
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-stackdriver/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-stackdriver
+[stackdriver-trace]: https://cloud.google.com/trace/
+[stackdriver-v1-api-url]: https://cloud.google.com/trace/docs/reference/v1/rpc/google.devtools.cloudtrace.v1#google.devtools.cloudtrace.v1.TraceSpan
diff --git a/exporters/trace/stackdriver/build.gradle b/exporters/trace/stackdriver/build.gradle
new file mode 100644
index 00000000..83dc970e
--- /dev/null
+++ b/exporters/trace/stackdriver/build.gradle
@@ -0,0 +1,31 @@
+description = 'OpenCensus Trace Stackdriver Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compileOnly libraries.auto_value
+
+ compile project(':opencensus-api'),
+ project(':opencensus-contrib-monitored-resource-util'),
+ libraries.google_auth,
+ libraries.guava
+
+ compile (libraries.google_cloud_trace) {
+ // Prefer library version.
+ exclude group: 'com.google.guava', module: 'guava'
+
+ // Prefer library version.
+ exclude group: 'com.google.code.findbugs', module: 'jsr305'
+
+ // We will always be more up to date.
+ exclude group: 'io.opencensus', module: 'opencensus-api'
+ }
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java
new file mode 100644
index 00000000..8797cc77
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverExporter.java
@@ -0,0 +1,148 @@
+/*
+ * 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.exporter.trace.stackdriver;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.io.IOException;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Stackdriver Trace.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * StackdriverExporter.createAndRegisterWithProjectId("MyStackdriverProjectId");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link StackdriverTraceExporter}.
+ * @since 0.6
+ */
+@Deprecated
+public final class StackdriverExporter {
+
+ /**
+ * Creates and registers the Stackdriver Trace exporter to the OpenCensus library for an explicit
+ * project ID and using explicit credentials. Only one Stackdriver exporter can be registered at
+ * any point.
+ *
+ * @param credentials a credentials used to authenticate API calls.
+ * @param projectId the cloud project id.
+ * @throws IllegalStateException if a Stackdriver exporter is already registered.
+ * @since 0.6
+ */
+ public static void createAndRegisterWithCredentialsAndProjectId(
+ Credentials credentials, String projectId) throws IOException {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder()
+ .setCredentials(credentials)
+ .setProjectId(projectId)
+ .build());
+ }
+
+ /**
+ * Creates and registers the Stackdriver Trace exporter to the OpenCensus library for an explicit
+ * project ID. Only one Stackdriver exporter can be registered at any point.
+ *
+ * <p>This uses the default application credentials see {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverExporter.createAndRegisterWithCredentialsAndProjectId(
+ * GoogleCredentials.getApplicationDefault(), projectId);
+ * }</pre>
+ *
+ * @param projectId the cloud project id.
+ * @throws IllegalStateException if a Stackdriver exporter is already registered.
+ * @since 0.6
+ */
+ public static void createAndRegisterWithProjectId(String projectId) throws IOException {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder()
+ .setCredentials(GoogleCredentials.getApplicationDefault())
+ .setProjectId(projectId)
+ .build());
+ }
+
+ /**
+ * Creates and registers the Stackdriver Trace exporter to the OpenCensus library. Only one
+ * Stackdriver exporter can be registered at any point.
+ *
+ * <p>This uses the default application credentials see {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>This uses the default project ID configured see {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * <p>This is equivalent with:
+ *
+ * <pre>{@code
+ * StackdriverExporter.createAndRegisterWithProjectId(ServiceOptions.getDefaultProjectId());
+ * }</pre>
+ *
+ * @throws IllegalStateException if a Stackdriver exporter is already registered.
+ * @since 0.6
+ */
+ public static void createAndRegister() throws IOException {
+ StackdriverTraceExporter.createAndRegister(
+ StackdriverTraceConfiguration.builder()
+ .setCredentials(GoogleCredentials.getApplicationDefault())
+ .setProjectId(ServiceOptions.getDefaultProjectId())
+ .build());
+ }
+
+ /**
+ * Registers the {@code StackdriverExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ StackdriverTraceExporter.register(spanExporter, handler);
+ }
+
+ /**
+ * Unregisters the Stackdriver Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Stackdriver exporter is not registered.
+ * @since 0.6
+ */
+ public static void unregister() {
+ StackdriverTraceExporter.unregister();
+ }
+
+ /**
+ * Unregisters the {@code StackdriverExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ StackdriverTraceExporter.unregister(spanExporter);
+ }
+
+ private StackdriverExporter() {}
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java
new file mode 100644
index 00000000..f78832d0
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfiguration.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018, 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.exporter.trace.stackdriver;
+
+import com.google.auth.Credentials;
+import com.google.auto.value.AutoValue;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Configurations for {@link StackdriverTraceExporter}.
+ *
+ * @since 0.12
+ */
+@AutoValue
+@Immutable
+public abstract class StackdriverTraceConfiguration {
+
+ StackdriverTraceConfiguration() {}
+
+ /**
+ * Returns the {@link Credentials}.
+ *
+ * @return the {@code Credentials}.
+ * @since 0.12
+ */
+ @Nullable
+ public abstract Credentials getCredentials();
+
+ /**
+ * Returns the cloud project id.
+ *
+ * @return the cloud project id.
+ * @since 0.12
+ */
+ @Nullable
+ public abstract String getProjectId();
+
+ /**
+ * Returns a TraceServiceStub instance used to make RPC calls.
+ *
+ * @return the trace service stub.
+ * @since 0.16
+ */
+ @Nullable
+ public abstract TraceServiceStub getTraceServiceStub();
+
+ /**
+ * Returns a new {@link Builder}.
+ *
+ * @return a {@code Builder}.
+ * @since 0.12
+ */
+ public static Builder builder() {
+ return new AutoValue_StackdriverTraceConfiguration.Builder();
+ }
+
+ /**
+ * Builder for {@link StackdriverTraceConfiguration}.
+ *
+ * @since 0.12
+ */
+ @AutoValue.Builder
+ public abstract static class Builder {
+
+ Builder() {}
+
+ /**
+ * Sets the {@link Credentials} used to authenticate API calls.
+ *
+ * @param credentials the {@code Credentials}.
+ * @return this.
+ * @since 0.12
+ */
+ public abstract Builder setCredentials(Credentials credentials);
+
+ /**
+ * Sets the cloud project id.
+ *
+ * @param projectId the cloud project id.
+ * @return this.
+ * @since 0.12
+ */
+ public abstract Builder setProjectId(String projectId);
+
+ /**
+ * Sets the trace service stub used to send gRPC calls.
+ *
+ * @param traceServiceStub the {@code TraceServiceStub}.
+ * @return this.
+ * @since 0.16
+ */
+ public abstract Builder setTraceServiceStub(TraceServiceStub traceServiceStub);
+
+ /**
+ * Builds a {@link StackdriverTraceConfiguration}.
+ *
+ * @return a {@code StackdriverTraceConfiguration}.
+ * @since 0.12
+ */
+ public abstract StackdriverTraceConfiguration build();
+ }
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java
new file mode 100644
index 00000000..0182ae94
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporter.java
@@ -0,0 +1,141 @@
+/*
+ * 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.exporter.trace.stackdriver;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.ServiceOptions;
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.io.IOException;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Stackdriver Trace.
+ *
+ * <p>Example of usage on Google Cloud VMs:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * StackdriverTraceExporter.createAndRegister(
+ * StackdriverTraceConfiguration.builder()
+ * .setProjectId("MyStackdriverProjectId")
+ * .build());
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class StackdriverTraceExporter {
+
+ private static final String REGISTER_NAME = StackdriverTraceExporter.class.getName();
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static Handler handler = null;
+
+ /**
+ * Creates and registers the Stackdriver Trace exporter to the OpenCensus library. Only one
+ * Stackdriver exporter can be registered at any point.
+ *
+ * <p>If the {@code credentials} in the provided {@link StackdriverTraceConfiguration} is not set,
+ * the exporter will use the default application credentials. See {@link
+ * GoogleCredentials#getApplicationDefault}.
+ *
+ * <p>If the {@code projectId} in the provided {@link StackdriverTraceConfiguration} is not set,
+ * the exporter will use the default project ID. See {@link ServiceOptions#getDefaultProjectId}.
+ *
+ * @param configuration the {@code StackdriverTraceConfiguration} used to create the exporter.
+ * @throws IllegalStateException if a Stackdriver exporter is already registered.
+ * @since 0.12
+ */
+ public static void createAndRegister(StackdriverTraceConfiguration configuration)
+ throws IOException {
+ synchronized (monitor) {
+ checkState(handler == null, "Stackdriver exporter is already registered.");
+ Credentials credentials = configuration.getCredentials();
+ String projectId = configuration.getProjectId();
+ projectId = projectId != null ? projectId : ServiceOptions.getDefaultProjectId();
+
+ StackdriverV2ExporterHandler handler;
+ TraceServiceStub stub = configuration.getTraceServiceStub();
+ if (stub == null) {
+ handler =
+ StackdriverV2ExporterHandler.createWithCredentials(
+ credentials != null ? credentials : GoogleCredentials.getApplicationDefault(),
+ projectId);
+ } else {
+ handler = new StackdriverV2ExporterHandler(projectId, TraceServiceClient.create(stub));
+ }
+
+ registerInternal(handler);
+ }
+ }
+
+ private static void registerInternal(Handler newHandler) {
+ synchronized (monitor) {
+ handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Registers the {@code StackdriverTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ /**
+ * Unregisters the Stackdriver Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Stackdriver exporter is not registered.
+ * @since 0.12
+ */
+ public static void unregister() {
+ synchronized (monitor) {
+ checkState(handler != null, "Stackdriver exporter is not registered.");
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ handler = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@code StackdriverTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+
+ private StackdriverTraceExporter() {}
+}
diff --git a/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java
new file mode 100644
index 00000000..de022c3f
--- /dev/null
+++ b/exporters/trace/stackdriver/src/main/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandler.java
@@ -0,0 +1,501 @@
+/*
+ * 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.exporter.trace.stackdriver;
+
+import static com.google.api.client.util.Preconditions.checkNotNull;
+
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.auth.Credentials;
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.TraceServiceSettings;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.cloudtrace.v2.AttributeValue;
+import com.google.devtools.cloudtrace.v2.AttributeValue.Builder;
+import com.google.devtools.cloudtrace.v2.ProjectName;
+import com.google.devtools.cloudtrace.v2.Span;
+import com.google.devtools.cloudtrace.v2.Span.Attributes;
+import com.google.devtools.cloudtrace.v2.Span.Link;
+import com.google.devtools.cloudtrace.v2.Span.Links;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent.MessageEvent;
+import com.google.devtools.cloudtrace.v2.SpanName;
+import com.google.devtools.cloudtrace.v2.TruncatableString;
+import com.google.protobuf.Int32Value;
+import com.google.rpc.Status;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.OpenCensusLibraryInformation;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResourceUtils;
+import io.opencensus.contrib.monitoredresource.util.ResourceType;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Exporter to Stackdriver Trace API v2. */
+final class StackdriverV2ExporterHandler extends SpanExporter.Handler {
+
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+ private static final String AGENT_LABEL_KEY = "g.co/agent";
+ private static final String AGENT_LABEL_VALUE_STRING =
+ "opencensus-java [" + OpenCensusLibraryInformation.VERSION + "]";
+ private static final String SERVER_PREFIX = "Recv.";
+ private static final String CLIENT_PREFIX = "Sent.";
+ private static final AttributeValue AGENT_LABEL_VALUE =
+ AttributeValue.newBuilder()
+ .setStringValue(toTruncatableStringProto(AGENT_LABEL_VALUE_STRING))
+ .build();
+
+ private static final ImmutableMap<String, String> HTTP_ATTRIBUTE_MAPPING =
+ ImmutableMap.<String, String>builder()
+ .put("http.host", "/http/host")
+ .put("http.method", "/http/method")
+ .put("http.path", "/http/path")
+ .put("http.route", "/http/route")
+ .put("http.user_agent", "/http/user_agent")
+ .put("http.status_code", "/http/status_code")
+ .build();
+
+ @javax.annotation.Nullable
+ private static final MonitoredResource RESOURCE = MonitoredResourceUtils.getDefaultResource();
+
+ // Only initialize once.
+ private static final Map<String, AttributeValue> RESOURCE_LABELS = getResourceLabels(RESOURCE);
+
+ // Constant functions for AttributeValue.
+ private static final Function<String, /*@Nullable*/ AttributeValue> stringAttributeValueFunction =
+ new Function<String, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(String stringValue) {
+ Builder attributeValueBuilder = AttributeValue.newBuilder();
+ attributeValueBuilder.setStringValue(toTruncatableStringProto(stringValue));
+ return attributeValueBuilder.build();
+ }
+ };
+ private static final Function<Boolean, /*@Nullable*/ AttributeValue>
+ booleanAttributeValueFunction =
+ new Function<Boolean, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Boolean booleanValue) {
+ Builder attributeValueBuilder = AttributeValue.newBuilder();
+ attributeValueBuilder.setBoolValue(booleanValue);
+ return attributeValueBuilder.build();
+ }
+ };
+ private static final Function<Long, /*@Nullable*/ AttributeValue> longAttributeValueFunction =
+ new Function<Long, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Long longValue) {
+ Builder attributeValueBuilder = AttributeValue.newBuilder();
+ attributeValueBuilder.setIntValue(longValue);
+ return attributeValueBuilder.build();
+ }
+ };
+ private static final Function<Double, /*@Nullable*/ AttributeValue> doubleAttributeValueFunction =
+ new Function<Double, /*@Nullable*/ AttributeValue>() {
+ @Override
+ public AttributeValue apply(Double doubleValue) {
+ Builder attributeValueBuilder = AttributeValue.newBuilder();
+ // TODO: set double value if Stackdriver Trace support it in the future.
+ attributeValueBuilder.setStringValue(
+ toTruncatableStringProto(String.valueOf(doubleValue)));
+ return attributeValueBuilder.build();
+ }
+ };
+
+ private final String projectId;
+ private final TraceServiceClient traceServiceClient;
+ private final ProjectName projectName;
+
+ @VisibleForTesting
+ StackdriverV2ExporterHandler(String projectId, TraceServiceClient traceServiceClient) {
+ this.projectId = checkNotNull(projectId, "projectId");
+ this.traceServiceClient = traceServiceClient;
+ projectName = ProjectName.of(this.projectId);
+
+ Tracing.getExportComponent()
+ .getSampledSpanStore()
+ .registerSpanNamesForCollection(Collections.singletonList("ExportStackdriverTraces"));
+ }
+
+ static StackdriverV2ExporterHandler createWithCredentials(
+ Credentials credentials, String projectId) throws IOException {
+ checkNotNull(credentials, "credentials");
+ TraceServiceSettings traceServiceSettings =
+ TraceServiceSettings.newBuilder()
+ .setCredentialsProvider(FixedCredentialsProvider.create(credentials))
+ .build();
+ return new StackdriverV2ExporterHandler(
+ projectId, TraceServiceClient.create(traceServiceSettings));
+ }
+
+ @VisibleForTesting
+ Span generateSpan(SpanData spanData, Map<String, AttributeValue> resourceLabels) {
+ SpanContext context = spanData.getContext();
+ final String spanIdHex = context.getSpanId().toLowerBase16();
+ SpanName spanName =
+ SpanName.newBuilder()
+ .setProject(projectId)
+ .setTrace(context.getTraceId().toLowerBase16())
+ .setSpan(spanIdHex)
+ .build();
+ Span.Builder spanBuilder =
+ Span.newBuilder()
+ .setName(spanName.toString())
+ .setSpanId(spanIdHex)
+ .setDisplayName(
+ toTruncatableStringProto(toDisplayName(spanData.getName(), spanData.getKind())))
+ .setStartTime(toTimestampProto(spanData.getStartTimestamp()))
+ .setAttributes(toAttributesProto(spanData.getAttributes(), resourceLabels))
+ .setTimeEvents(
+ toTimeEventsProto(spanData.getAnnotations(), spanData.getMessageEvents()));
+ io.opencensus.trace.Status status = spanData.getStatus();
+ if (status != null) {
+ spanBuilder.setStatus(toStatusProto(status));
+ }
+ Timestamp end = spanData.getEndTimestamp();
+ if (end != null) {
+ spanBuilder.setEndTime(toTimestampProto(end));
+ }
+ spanBuilder.setLinks(toLinksProto(spanData.getLinks()));
+ Integer childSpanCount = spanData.getChildSpanCount();
+ if (childSpanCount != null) {
+ spanBuilder.setChildSpanCount(Int32Value.newBuilder().setValue(childSpanCount).build());
+ }
+ if (spanData.getParentSpanId() != null && spanData.getParentSpanId().isValid()) {
+ spanBuilder.setParentSpanId(spanData.getParentSpanId().toLowerBase16());
+ }
+
+ return spanBuilder.build();
+ }
+
+ private static Span.TimeEvents toTimeEventsProto(
+ TimedEvents<Annotation> annotationTimedEvents,
+ TimedEvents<io.opencensus.trace.MessageEvent> messageEventTimedEvents) {
+ Span.TimeEvents.Builder timeEventsBuilder = Span.TimeEvents.newBuilder();
+ timeEventsBuilder.setDroppedAnnotationsCount(annotationTimedEvents.getDroppedEventsCount());
+ for (TimedEvent<Annotation> annotation : annotationTimedEvents.getEvents()) {
+ timeEventsBuilder.addTimeEvent(toTimeAnnotationProto(annotation));
+ }
+ timeEventsBuilder.setDroppedMessageEventsCount(messageEventTimedEvents.getDroppedEventsCount());
+ for (TimedEvent<io.opencensus.trace.MessageEvent> networkEvent :
+ messageEventTimedEvents.getEvents()) {
+ timeEventsBuilder.addTimeEvent(toTimeMessageEventProto(networkEvent));
+ }
+ return timeEventsBuilder.build();
+ }
+
+ private static TimeEvent toTimeAnnotationProto(TimedEvent<Annotation> timedEvent) {
+ TimeEvent.Builder timeEventBuilder =
+ TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+ Annotation annotation = timedEvent.getEvent();
+ timeEventBuilder.setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(toTruncatableStringProto(annotation.getDescription()))
+ .setAttributes(toAttributesBuilderProto(annotation.getAttributes(), 0))
+ .build());
+ return timeEventBuilder.build();
+ }
+
+ private static TimeEvent toTimeMessageEventProto(
+ TimedEvent<io.opencensus.trace.MessageEvent> timedEvent) {
+ TimeEvent.Builder timeEventBuilder =
+ TimeEvent.newBuilder().setTime(toTimestampProto(timedEvent.getTimestamp()));
+ io.opencensus.trace.MessageEvent messageEvent = timedEvent.getEvent();
+ timeEventBuilder.setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setId(messageEvent.getMessageId())
+ .setCompressedSizeBytes(messageEvent.getCompressedMessageSize())
+ .setUncompressedSizeBytes(messageEvent.getUncompressedMessageSize())
+ .setType(toMessageEventTypeProto(messageEvent))
+ .build());
+ return timeEventBuilder.build();
+ }
+
+ private static TimeEvent.MessageEvent.Type toMessageEventTypeProto(
+ io.opencensus.trace.MessageEvent messageEvent) {
+ if (messageEvent.getType() == Type.RECEIVED) {
+ return MessageEvent.Type.RECEIVED;
+ } else {
+ return MessageEvent.Type.SENT;
+ }
+ }
+
+ // These are the attributes of the Span, where usually we may add more attributes like the agent.
+ private static Attributes toAttributesProto(
+ io.opencensus.trace.export.SpanData.Attributes attributes,
+ Map<String, AttributeValue> resourceLabels) {
+ Attributes.Builder attributesBuilder =
+ toAttributesBuilderProto(
+ attributes.getAttributeMap(), attributes.getDroppedAttributesCount());
+ attributesBuilder.putAttributeMap(AGENT_LABEL_KEY, AGENT_LABEL_VALUE);
+ for (Entry<String, AttributeValue> entry : resourceLabels.entrySet()) {
+ attributesBuilder.putAttributeMap(entry.getKey(), entry.getValue());
+ }
+ return attributesBuilder.build();
+ }
+
+ private static Attributes.Builder toAttributesBuilderProto(
+ Map<String, io.opencensus.trace.AttributeValue> attributes, int droppedAttributesCount) {
+ Attributes.Builder attributesBuilder =
+ Attributes.newBuilder().setDroppedAttributesCount(droppedAttributesCount);
+ for (Map.Entry<String, io.opencensus.trace.AttributeValue> label : attributes.entrySet()) {
+ AttributeValue value = toAttributeValueProto(label.getValue());
+ if (value != null) {
+ attributesBuilder.putAttributeMap(mapKey(label.getKey()), value);
+ }
+ }
+ return attributesBuilder;
+ }
+
+ @VisibleForTesting
+ static Map<String, AttributeValue> getResourceLabels(
+ @javax.annotation.Nullable MonitoredResource resource) {
+ if (resource == null) {
+ return Collections.emptyMap();
+ }
+ Map<String, AttributeValue> resourceLabels = new HashMap<String, AttributeValue>();
+ ResourceType resourceType = resource.getResourceType();
+ switch (resourceType) {
+ case AWS_EC2_INSTANCE:
+ AwsEc2InstanceMonitoredResource awsEc2InstanceMonitoredResource =
+ (AwsEc2InstanceMonitoredResource) resource;
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "aws_account",
+ awsEc2InstanceMonitoredResource.getAccount());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "instance_id",
+ awsEc2InstanceMonitoredResource.getInstanceId());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "region",
+ "aws:" + awsEc2InstanceMonitoredResource.getRegion());
+ return Collections.unmodifiableMap(resourceLabels);
+ case GCP_GCE_INSTANCE:
+ GcpGceInstanceMonitoredResource gcpGceInstanceMonitoredResource =
+ (GcpGceInstanceMonitoredResource) resource;
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "project_id",
+ gcpGceInstanceMonitoredResource.getAccount());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "instance_id",
+ gcpGceInstanceMonitoredResource.getInstanceId());
+ putToResourceAttributeMap(
+ resourceLabels, resourceType, "zone", gcpGceInstanceMonitoredResource.getZone());
+ return Collections.unmodifiableMap(resourceLabels);
+ case GCP_GKE_CONTAINER:
+ GcpGkeContainerMonitoredResource gcpGkeContainerMonitoredResource =
+ (GcpGkeContainerMonitoredResource) resource;
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "project_id",
+ gcpGkeContainerMonitoredResource.getAccount());
+ putToResourceAttributeMap(
+ resourceLabels, resourceType, "location", gcpGkeContainerMonitoredResource.getZone());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "cluster_name",
+ gcpGkeContainerMonitoredResource.getClusterName());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "container_name",
+ gcpGkeContainerMonitoredResource.getContainerName());
+ putToResourceAttributeMap(
+ resourceLabels,
+ resourceType,
+ "namespace_name",
+ gcpGkeContainerMonitoredResource.getNamespaceId());
+ putToResourceAttributeMap(
+ resourceLabels, resourceType, "pod_name", gcpGkeContainerMonitoredResource.getPodId());
+ return Collections.unmodifiableMap(resourceLabels);
+ }
+ return Collections.emptyMap();
+ }
+
+ private static void putToResourceAttributeMap(
+ Map<String, AttributeValue> map,
+ ResourceType resourceType,
+ String attributeName,
+ String attributeValue) {
+ map.put(
+ createResourceLabelKey(resourceType, attributeName),
+ toStringAttributeValueProto(attributeValue));
+ }
+
+ @VisibleForTesting
+ static String createResourceLabelKey(ResourceType resourceType, String resourceAttribute) {
+ return String.format("g.co/r/%s/%s", mapToStringResourceType(resourceType), resourceAttribute);
+ }
+
+ private static String mapToStringResourceType(ResourceType resourceType) {
+ switch (resourceType) {
+ case GCP_GCE_INSTANCE:
+ return "gce_instance";
+ case GCP_GKE_CONTAINER:
+ return "k8s_container";
+ case AWS_EC2_INSTANCE:
+ return "aws_ec2_instance";
+ }
+ throw new IllegalArgumentException("Unknown resource type.");
+ }
+
+ @VisibleForTesting
+ static AttributeValue toStringAttributeValueProto(String value) {
+ return AttributeValue.newBuilder().setStringValue(toTruncatableStringProto(value)).build();
+ }
+
+ private static String mapKey(String key) {
+ if (HTTP_ATTRIBUTE_MAPPING.containsKey(key)) {
+ return HTTP_ATTRIBUTE_MAPPING.get(key);
+ } else {
+ return key;
+ }
+ }
+
+ private static Status toStatusProto(io.opencensus.trace.Status status) {
+ Status.Builder statusBuilder = Status.newBuilder().setCode(status.getCanonicalCode().value());
+ if (status.getDescription() != null) {
+ statusBuilder.setMessage(status.getDescription());
+ }
+ return statusBuilder.build();
+ }
+
+ private static TruncatableString toTruncatableStringProto(String string) {
+ return TruncatableString.newBuilder().setValue(string).setTruncatedByteCount(0).build();
+ }
+
+ private static com.google.protobuf.Timestamp toTimestampProto(Timestamp timestamp) {
+ return com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(timestamp.getSeconds())
+ .setNanos(timestamp.getNanos())
+ .build();
+ }
+
+ @javax.annotation.Nullable
+ private static AttributeValue toAttributeValueProto(
+ io.opencensus.trace.AttributeValue attributeValue) {
+ return attributeValue.match(
+ stringAttributeValueFunction,
+ booleanAttributeValueFunction,
+ longAttributeValueFunction,
+ doubleAttributeValueFunction,
+ Functions.</*@Nullable*/ AttributeValue>returnNull());
+ }
+
+ private static Link.Type toLinkTypeProto(io.opencensus.trace.Link.Type type) {
+ if (type == io.opencensus.trace.Link.Type.PARENT_LINKED_SPAN) {
+ return Link.Type.PARENT_LINKED_SPAN;
+ } else {
+ return Link.Type.CHILD_LINKED_SPAN;
+ }
+ }
+
+ private static String toDisplayName(String spanName, @javax.annotation.Nullable Kind spanKind) {
+ if (spanKind == Kind.SERVER && !spanName.startsWith(SERVER_PREFIX)) {
+ return SERVER_PREFIX + spanName;
+ }
+
+ if (spanKind == Kind.CLIENT && !spanName.startsWith(CLIENT_PREFIX)) {
+ return CLIENT_PREFIX + spanName;
+ }
+
+ return spanName;
+ }
+
+ private static Link toLinkProto(io.opencensus.trace.Link link) {
+ checkNotNull(link);
+ return Link.newBuilder()
+ .setTraceId(link.getTraceId().toLowerBase16())
+ .setSpanId(link.getSpanId().toLowerBase16())
+ .setType(toLinkTypeProto(link.getType()))
+ .setAttributes(toAttributesBuilderProto(link.getAttributes(), 0))
+ .build();
+ }
+
+ private static Links toLinksProto(io.opencensus.trace.export.SpanData.Links links) {
+ final Links.Builder linksBuilder =
+ Links.newBuilder().setDroppedLinksCount(links.getDroppedLinksCount());
+ for (io.opencensus.trace.Link link : links.getLinks()) {
+ linksBuilder.addLink(toLinkProto(link));
+ }
+ return linksBuilder.build();
+ }
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+ // sets the default sampler to always sample and we get the gRPC span of the stackdriver
+ // export call always sampled and go to an infinite loop.
+ Scope scope =
+ tracer
+ .spanBuilder("ExportStackdriverTraces")
+ .setSampler(probabilitySampler)
+ .setRecordEvents(true)
+ .startScopedSpan();
+ try {
+ List<Span> spans = new ArrayList<>(spanDataList.size());
+ for (SpanData spanData : spanDataList) {
+ spans.add(generateSpan(spanData, RESOURCE_LABELS));
+ }
+ // Sync call because it is already called for a batch of data, and on a separate thread.
+ // TODO(bdrutu): Consider to make this async in the future.
+ traceServiceClient.batchWriteSpans(projectName, spans);
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java
new file mode 100644
index 00000000..6926e869
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceConfigurationTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018, 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.exporter.trace.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import java.util.Date;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link StackdriverTraceConfiguration}. */
+@RunWith(JUnit4.class)
+public class StackdriverTraceConfigurationTest {
+
+ private static final Credentials FAKE_CREDENTIALS =
+ GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+ private static final String PROJECT_ID = "project";
+
+ @Test
+ public void defaultConfiguration() {
+ StackdriverTraceConfiguration configuration = StackdriverTraceConfiguration.builder().build();
+ assertThat(configuration.getCredentials()).isNull();
+ assertThat(configuration.getProjectId()).isNull();
+ }
+
+ @Test
+ public void updateAll() {
+ StackdriverTraceConfiguration configuration =
+ StackdriverTraceConfiguration.builder()
+ .setCredentials(FAKE_CREDENTIALS)
+ .setProjectId(PROJECT_ID)
+ .build();
+ assertThat(configuration.getCredentials()).isEqualTo(FAKE_CREDENTIALS);
+ assertThat(configuration.getProjectId()).isEqualTo(PROJECT_ID);
+ }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java
new file mode 100644
index 00000000..6a12a899
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.exporter.trace.stackdriver;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link StackdriverTraceExporter}. */
+@RunWith(JUnit4.class)
+public class StackdriverTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+ @Mock private Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterStackdriverExporter() {
+ StackdriverTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter"), same(handler));
+ StackdriverTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.stackdriver.StackdriverTraceExporter"));
+ }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java
new file mode 100644
index 00000000..32458597
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerExportTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018, 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.exporter.trace.stackdriver;
+
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.trace.v2.TraceServiceClient;
+import com.google.cloud.trace.v2.stub.TraceServiceStub;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for exporting in {@link StackdriverV2ExporterHandler}. */
+@RunWith(JUnit4.class)
+public final class StackdriverV2ExporterHandlerExportTest {
+ private static final String PROJECT_ID = "PROJECT_ID";
+ // mock the service stub to provide a fake trace service.
+ @Mock private TraceServiceStub traceServiceStub;
+ private TraceServiceClient traceServiceClient;
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private StackdriverV2ExporterHandler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ // TODO(@Hailong): TraceServiceClient.create(TraceServiceStub) is a beta API and might change
+ // in the future.
+ traceServiceClient = TraceServiceClient.create(traceServiceStub);
+ handler = new StackdriverV2ExporterHandler(PROJECT_ID, traceServiceClient);
+ }
+
+ @Test
+ public void export() {
+ when(traceServiceStub.batchWriteSpansCallable())
+ .thenThrow(new RuntimeException("TraceServiceStub called"));
+ Collection<SpanData> spanDataList = Collections.<SpanData>emptyList();
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("TraceServiceStub called");
+ handler.export(spanDataList);
+ }
+}
diff --git a/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java
new file mode 100644
index 00000000..8b28dc06
--- /dev/null
+++ b/exporters/trace/stackdriver/src/test/java/io/opencensus/exporter/trace/stackdriver/StackdriverV2ExporterHandlerProtoTest.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright 2018, 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.exporter.trace.stackdriver;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.AWS_EC2_INSTANCE;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.GCP_GCE_INSTANCE;
+import static io.opencensus.contrib.monitoredresource.util.ResourceType.GCP_GKE_CONTAINER;
+import static io.opencensus.exporter.trace.stackdriver.StackdriverV2ExporterHandler.createResourceLabelKey;
+import static io.opencensus.exporter.trace.stackdriver.StackdriverV2ExporterHandler.toStringAttributeValueProto;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.cloudtrace.v2.AttributeValue;
+import com.google.devtools.cloudtrace.v2.Span;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent;
+import com.google.devtools.cloudtrace.v2.Span.TimeEvent.MessageEvent;
+import com.google.devtools.cloudtrace.v2.StackTrace;
+import com.google.devtools.cloudtrace.v2.TruncatableString;
+import com.google.protobuf.Int32Value;
+import io.opencensus.common.Timestamp;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.AwsEc2InstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGceInstanceMonitoredResource;
+import io.opencensus.contrib.monitoredresource.util.MonitoredResource.GcpGkeContainerMonitoredResource;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for proto conversions in {@link StackdriverV2ExporterHandler}. */
+@RunWith(JUnit4.class)
+public final class StackdriverV2ExporterHandlerProtoTest {
+
+ private static final Credentials FAKE_CREDENTIALS =
+ GoogleCredentials.newBuilder().setAccessToken(new AccessToken("fake", new Date(100))).build();
+ // OpenCensus constants
+ private static final Timestamp startTimestamp = Timestamp.create(123, 456);
+ private static final Timestamp eventTimestamp1 = Timestamp.create(123, 457);
+ private static final Timestamp eventTimestamp2 = Timestamp.create(123, 458);
+ private static final Timestamp eventTimestamp3 = Timestamp.create(123, 459);
+ private static final Timestamp endTimestamp = Timestamp.create(123, 460);
+
+ private static final String PROJECT_ID = "PROJECT_ID";
+ private static final String TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736";
+ private static final String SPAN_ID = "24aa0b2d371f48c9";
+ private static final String PARENT_SPAN_ID = "71da8d631536f5f1";
+ private static final String SPAN_NAME = "MySpanName";
+ private static final String SD_SPAN_NAME =
+ String.format("projects/%s/traces/%s/spans/%s", PROJECT_ID, TRACE_ID, SPAN_ID);
+ private static final String ANNOTATION_TEXT = "MyAnnotationText";
+ private static final String ATTRIBUTE_KEY_1 = "MyAttributeKey1";
+ private static final String ATTRIBUTE_KEY_2 = "MyAttributeKey2";
+
+ private static final int DROPPED_ATTRIBUTES_COUNT = 1;
+ private static final int DROPPED_ANNOTATIONS_COUNT = 2;
+ private static final int DROPPED_NETWORKEVENTS_COUNT = 3;
+ private static final int DROPPED_LINKS_COUNT = 4;
+ private static final int CHILD_SPAN_COUNT = 13;
+
+ private static final Annotation annotation = Annotation.fromDescription(ANNOTATION_TEXT);
+ private static final io.opencensus.trace.MessageEvent recvMessageEvent =
+ io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.RECEIVED, 1)
+ .build();
+ private static final io.opencensus.trace.MessageEvent sentMessageEvent =
+ io.opencensus.trace.MessageEvent.builder(io.opencensus.trace.MessageEvent.Type.SENT, 1)
+ .build();
+ private static final Status status = Status.DEADLINE_EXCEEDED.withDescription("TooSlow");
+ private static final SpanId parentSpanId = SpanId.fromLowerBase16(PARENT_SPAN_ID);
+ private static final SpanId spanId = SpanId.fromLowerBase16(SPAN_ID);
+ private static final TraceId traceId = TraceId.fromLowerBase16(TRACE_ID);
+ private static final TraceOptions traceOptions = TraceOptions.DEFAULT;
+ private static final SpanContext spanContext = SpanContext.create(traceId, spanId, traceOptions);
+
+ private static final List<TimedEvent<Annotation>> annotationsList =
+ ImmutableList.of(
+ SpanData.TimedEvent.create(eventTimestamp1, annotation),
+ SpanData.TimedEvent.create(eventTimestamp3, annotation));
+ private static final List<TimedEvent<io.opencensus.trace.MessageEvent>> networkEventsList =
+ ImmutableList.of(
+ SpanData.TimedEvent.create(eventTimestamp1, recvMessageEvent),
+ SpanData.TimedEvent.create(eventTimestamp2, sentMessageEvent));
+ private static final List<Link> linksList =
+ ImmutableList.of(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN));
+
+ private static final SpanData.Attributes attributes =
+ SpanData.Attributes.create(
+ ImmutableMap.of(
+ ATTRIBUTE_KEY_1,
+ io.opencensus.trace.AttributeValue.longAttributeValue(10L),
+ ATTRIBUTE_KEY_2,
+ io.opencensus.trace.AttributeValue.booleanAttributeValue(true)),
+ DROPPED_ATTRIBUTES_COUNT);
+ private static final TimedEvents<Annotation> annotations =
+ TimedEvents.create(annotationsList, DROPPED_ANNOTATIONS_COUNT);
+ private static final TimedEvents<io.opencensus.trace.MessageEvent> messageEvents =
+ TimedEvents.create(networkEventsList, DROPPED_NETWORKEVENTS_COUNT);
+ private static final SpanData.Links links = SpanData.Links.create(linksList, DROPPED_LINKS_COUNT);
+ private static final Map<String, AttributeValue> EMPTY_RESOURCE_LABELS = Collections.emptyMap();
+ private static final AwsEc2InstanceMonitoredResource AWS_EC2_INSTANCE_MONITORED_RESOURCE =
+ AwsEc2InstanceMonitoredResource.create("my-project", "my-instance", "us-east-1");
+ private static final GcpGceInstanceMonitoredResource GCP_GCE_INSTANCE_MONITORED_RESOURCE =
+ GcpGceInstanceMonitoredResource.create("my-project", "my-instance", "us-east1");
+ private static final GcpGkeContainerMonitoredResource GCP_GKE_CONTAINER_MONITORED_RESOURCE =
+ GcpGkeContainerMonitoredResource.create(
+ "my-project", "cluster", "container", "namespace", "my-instance", "pod", "us-east1");
+ private static final ImmutableMap<String, AttributeValue> AWS_RESOURCE_LABELS =
+ ImmutableMap.of(
+ createResourceLabelKey(AWS_EC2_INSTANCE, "aws_account"),
+ toStringAttributeValueProto("my-project"),
+ createResourceLabelKey(AWS_EC2_INSTANCE, "instance_id"),
+ toStringAttributeValueProto("my-instance"),
+ createResourceLabelKey(AWS_EC2_INSTANCE, "region"),
+ toStringAttributeValueProto("aws:us-east-1"));
+ private static final ImmutableMap<String, AttributeValue> GCE_RESOURCE_LABELS =
+ ImmutableMap.of(
+ createResourceLabelKey(GCP_GCE_INSTANCE, "project_id"),
+ toStringAttributeValueProto("my-project"),
+ createResourceLabelKey(GCP_GCE_INSTANCE, "instance_id"),
+ toStringAttributeValueProto("my-instance"),
+ createResourceLabelKey(GCP_GCE_INSTANCE, "zone"),
+ toStringAttributeValueProto("us-east1"));
+ private static final ImmutableMap<String, AttributeValue> GKE_RESOURCE_LABELS =
+ ImmutableMap.<String, AttributeValue>builder()
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "project_id"),
+ toStringAttributeValueProto("my-project"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "cluster_name"),
+ toStringAttributeValueProto("cluster"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "container_name"),
+ toStringAttributeValueProto("container"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "namespace_name"),
+ toStringAttributeValueProto("namespace"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "pod_name"),
+ toStringAttributeValueProto("pod"))
+ .put(
+ createResourceLabelKey(GCP_GKE_CONTAINER, "location"),
+ toStringAttributeValueProto("us-east1"))
+ .build();
+
+ private StackdriverV2ExporterHandler handler;
+
+ @Before
+ public void setUp() throws IOException {
+ handler = StackdriverV2ExporterHandler.createWithCredentials(FAKE_CREDENTIALS, PROJECT_ID);
+ }
+
+ @Test
+ public void generateSpan() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ null,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ TimeEvent annotationTimeEvent1 =
+ TimeEvent.newBuilder()
+ .setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(
+ TruncatableString.newBuilder().setValue(ANNOTATION_TEXT).build())
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp1.getSeconds())
+ .setNanos(eventTimestamp1.getNanos())
+ .build())
+ .build();
+ TimeEvent annotationTimeEvent2 =
+ TimeEvent.newBuilder()
+ .setAnnotation(
+ TimeEvent.Annotation.newBuilder()
+ .setDescription(
+ TruncatableString.newBuilder().setValue(ANNOTATION_TEXT).build())
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp3.getSeconds())
+ .setNanos(eventTimestamp3.getNanos())
+ .build())
+ .build();
+
+ TimeEvent sentTimeEvent =
+ TimeEvent.newBuilder()
+ .setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setType(MessageEvent.Type.SENT)
+ .setId(sentMessageEvent.getMessageId()))
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp2.getSeconds())
+ .setNanos(eventTimestamp2.getNanos())
+ .build())
+ .build();
+ TimeEvent recvTimeEvent =
+ TimeEvent.newBuilder()
+ .setMessageEvent(
+ TimeEvent.MessageEvent.newBuilder()
+ .setType(MessageEvent.Type.RECEIVED)
+ .setId(recvMessageEvent.getMessageId()))
+ .setTime(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(eventTimestamp1.getSeconds())
+ .setNanos(eventTimestamp1.getNanos())
+ .build())
+ .build();
+
+ Span.Links spanLinks =
+ Span.Links.newBuilder()
+ .setDroppedLinksCount(DROPPED_LINKS_COUNT)
+ .addLink(
+ Span.Link.newBuilder()
+ .setType(Span.Link.Type.CHILD_LINKED_SPAN)
+ .setTraceId(TRACE_ID)
+ .setSpanId(SPAN_ID)
+ .setAttributes(Span.Attributes.newBuilder().build())
+ .build())
+ .build();
+
+ com.google.rpc.Status spanStatus =
+ com.google.rpc.Status.newBuilder()
+ .setCode(com.google.rpc.Code.DEADLINE_EXCEEDED.getNumber())
+ .setMessage("TooSlow")
+ .build();
+
+ com.google.protobuf.Timestamp startTime =
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(startTimestamp.getSeconds())
+ .setNanos(startTimestamp.getNanos())
+ .build();
+ com.google.protobuf.Timestamp endTime =
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(endTimestamp.getSeconds())
+ .setNanos(endTimestamp.getNanos())
+ .build();
+
+ Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS);
+ assertThat(span.getName()).isEqualTo(SD_SPAN_NAME);
+ assertThat(span.getSpanId()).isEqualTo(SPAN_ID);
+ assertThat(span.getParentSpanId()).isEqualTo(PARENT_SPAN_ID);
+ assertThat(span.getDisplayName())
+ .isEqualTo(TruncatableString.newBuilder().setValue(SPAN_NAME).build());
+ assertThat(span.getStartTime()).isEqualTo(startTime);
+ assertThat(span.getEndTime()).isEqualTo(endTime);
+ assertThat(span.getAttributes().getDroppedAttributesCount())
+ .isEqualTo(DROPPED_ATTRIBUTES_COUNT);
+ // The generated attributes map contains more values (e.g. agent). We only test what we added.
+ assertThat(span.getAttributes().getAttributeMapMap())
+ .containsEntry(ATTRIBUTE_KEY_1, AttributeValue.newBuilder().setIntValue(10L).build());
+ assertThat(span.getAttributes().getAttributeMapMap())
+ .containsEntry(ATTRIBUTE_KEY_2, AttributeValue.newBuilder().setBoolValue(true).build());
+ // TODO(@Hailong): add stack trace test in the future.
+ assertThat(span.getStackTrace()).isEqualTo(StackTrace.newBuilder().build());
+ assertThat(span.getTimeEvents().getDroppedMessageEventsCount())
+ .isEqualTo(DROPPED_NETWORKEVENTS_COUNT);
+ assertThat(span.getTimeEvents().getDroppedAnnotationsCount())
+ .isEqualTo(DROPPED_ANNOTATIONS_COUNT);
+ assertThat(span.getTimeEvents().getTimeEventList())
+ .containsAllOf(annotationTimeEvent1, annotationTimeEvent2, sentTimeEvent, recvTimeEvent);
+ assertThat(span.getLinks()).isEqualTo(spanLinks);
+ assertThat(span.getStatus()).isEqualTo(spanStatus);
+ assertThat(span.getSameProcessAsParentSpan())
+ .isEqualTo(com.google.protobuf.BoolValue.newBuilder().build());
+ assertThat(span.getChildSpanCount())
+ .isEqualTo(Int32Value.newBuilder().setValue(CHILD_SPAN_COUNT).build());
+ }
+
+ @Test
+ public void getResourceLabels_AwsEc2ResourceLabels() {
+ testGetResourceLabels(AWS_EC2_INSTANCE_MONITORED_RESOURCE, AWS_RESOURCE_LABELS);
+ }
+
+ @Test
+ public void getResourceLabels_GceResourceLabels() {
+ testGetResourceLabels(GCP_GCE_INSTANCE_MONITORED_RESOURCE, GCE_RESOURCE_LABELS);
+ }
+
+ @Test
+ public void getResourceLabels_GkeResourceLabels() {
+ testGetResourceLabels(GCP_GKE_CONTAINER_MONITORED_RESOURCE, GKE_RESOURCE_LABELS);
+ }
+
+ private static void testGetResourceLabels(
+ MonitoredResource resource, Map<String, AttributeValue> expectedLabels) {
+ Map<String, AttributeValue> actualLabels =
+ StackdriverV2ExporterHandler.getResourceLabels(resource);
+ assertThat(actualLabels).containsExactlyEntriesIn(expectedLabels);
+ }
+
+ @Test
+ public void generateSpan_WithResourceLabels() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ null,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ Span span = handler.generateSpan(spanData, AWS_RESOURCE_LABELS);
+ Map<String, AttributeValue> attributeMap = span.getAttributes().getAttributeMapMap();
+ assertThat(attributeMap.entrySet()).containsAllIn(AWS_RESOURCE_LABELS.entrySet());
+ }
+
+ @Test
+ public void mapHttpAttributes() {
+ Map<String, io.opencensus.trace.AttributeValue> attributesMap =
+ new HashMap<String, io.opencensus.trace.AttributeValue>();
+
+ attributesMap.put("http.host", io.opencensus.trace.AttributeValue.stringAttributeValue("host"));
+ attributesMap.put(
+ "http.method", io.opencensus.trace.AttributeValue.stringAttributeValue("method"));
+ attributesMap.put("http.path", io.opencensus.trace.AttributeValue.stringAttributeValue("path"));
+ attributesMap.put(
+ "http.route", io.opencensus.trace.AttributeValue.stringAttributeValue("route"));
+ attributesMap.put(
+ "http.user_agent", io.opencensus.trace.AttributeValue.stringAttributeValue("user_agent"));
+ attributesMap.put(
+ "http.status_code", io.opencensus.trace.AttributeValue.longAttributeValue(200L));
+ SpanData.Attributes httpAttributes = SpanData.Attributes.create(attributesMap, 0);
+
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ startTimestamp,
+ httpAttributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+
+ Span span = handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS);
+ Map<String, AttributeValue> attributes = span.getAttributes().getAttributeMapMap();
+
+ assertThat(attributes).containsEntry("/http/host", toStringAttributeValueProto("host"));
+ assertThat(attributes).containsEntry("/http/method", toStringAttributeValueProto("method"));
+ assertThat(attributes).containsEntry("/http/path", toStringAttributeValueProto("path"));
+ assertThat(attributes).containsEntry("/http/route", toStringAttributeValueProto("route"));
+ assertThat(attributes)
+ .containsEntry("/http/user_agent", toStringAttributeValueProto("user_agent"));
+ assertThat(attributes)
+ .containsEntry("/http/status_code", AttributeValue.newBuilder().setIntValue(200L).build());
+ }
+
+ @Test
+ public void generateSpanName_ForServer() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ Kind.SERVER,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+ .isEqualTo("Recv." + SPAN_NAME);
+ }
+
+ @Test
+ public void generateSpanName_ForServerWithRecv() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ "Recv." + SPAN_NAME,
+ Kind.SERVER,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+ .isEqualTo("Recv." + SPAN_NAME);
+ }
+
+ @Test
+ public void generateSpanName_ForClient() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+ .isEqualTo("Sent." + SPAN_NAME);
+ }
+
+ @Test
+ public void generateSpanName_ForClientWithSent() {
+ SpanData spanData =
+ SpanData.create(
+ spanContext,
+ parentSpanId,
+ /* hasRemoteParent= */ true,
+ "Sent." + SPAN_NAME,
+ Kind.CLIENT,
+ startTimestamp,
+ attributes,
+ annotations,
+ messageEvents,
+ links,
+ CHILD_SPAN_COUNT,
+ status,
+ endTimestamp);
+ assertThat(handler.generateSpan(spanData, EMPTY_RESOURCE_LABELS).getDisplayName().getValue())
+ .isEqualTo("Sent." + SPAN_NAME);
+ }
+}
diff --git a/exporters/trace/zipkin/README.md b/exporters/trace/zipkin/README.md
new file mode 100644
index 00000000..4398360d
--- /dev/null
+++ b/exporters/trace/zipkin/README.md
@@ -0,0 +1,82 @@
+# OpenCensus Zipkin Trace Exporter
+[![Build Status][travis-image]][travis-url]
+[![Windows Build Status][appveyor-image]][appveyor-url]
+[![Maven Central][maven-image]][maven-url]
+
+The *OpenCensus Zipkin Trace Exporter* is a trace exporter that exports
+data to Zipkin. [Zipkin](http://zipkin.io/) Zipkin is a distributed
+tracing system. It helps gather timing data needed to troubleshoot
+latency problems in microservice architectures. It manages both the
+collection and lookup of this data.
+
+## Quickstart
+
+### Prerequisites
+
+[Zipkin](http://zipkin.io/) stores and queries traces exported by
+applications instrumented with Census. The easiest way to start a zipkin
+server is to paste the below:
+
+```bash
+wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
+java -jar zipkin.jar
+```
+
+
+### Hello Zipkin
+
+#### Add the dependencies to your project
+
+For Maven add to your `pom.xml`:
+```xml
+<dependencies>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-api</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-exporter-trace-zipkin</artifactId>
+ <version>0.16.1</version>
+ </dependency>
+ <dependency>
+ <groupId>io.opencensus</groupId>
+ <artifactId>opencensus-impl</artifactId>
+ <version>0.16.1</version>
+ <scope>runtime</scope>
+ </dependency>
+</dependencies>
+```
+
+For Gradle add to your dependencies:
+```groovy
+compile 'io.opencensus:opencensus-api:0.16.1'
+compile 'io.opencensus:opencensus-exporter-trace-zipkin:0.16.1'
+runtime 'io.opencensus:opencensus-impl:0.16.1'
+```
+
+#### Register the exporter
+
+This will report Zipkin v2 json format to a single server. Alternate
+[senders](https://github.com/openzipkin/zipkin-reporter-java) are available.
+
+```java
+public class MyMainClass {
+ public static void main(String[] args) throws Exception {
+ ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "my-service");
+ // ...
+ }
+}
+```
+
+#### Java Versions
+
+Java 6 or above is required for using this exporter.
+
+[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master
+[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java
+[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true
+[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master
+[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-zipkin/badge.svg
+[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-exporter-trace-zipkin
diff --git a/exporters/trace/zipkin/build.gradle b/exporters/trace/zipkin/build.gradle
new file mode 100644
index 00000000..530dff7d
--- /dev/null
+++ b/exporters/trace/zipkin/build.gradle
@@ -0,0 +1,18 @@
+description = 'OpenCensus Trace Zipkin Exporter'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.6
+ it.targetCompatibility = 1.6
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava,
+ libraries.zipkin_reporter,
+ libraries.zipkin_urlconnection
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java
new file mode 100644
index 00000000..e20360e8
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporter.java
@@ -0,0 +1,104 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Zipkin.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * ZipkinExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "myservicename");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @deprecated Deprecated due to inconsistent naming. Use {@link ZipkinTraceExporter}.
+ * @since 0.8
+ */
+@Deprecated
+public final class ZipkinExporter {
+
+ private ZipkinExporter() {}
+
+ /**
+ * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+ * exporter can be registered at any point.
+ *
+ * @param v2Url Ex http://127.0.0.1:9411/api/v2/spans
+ * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+ * @throws IllegalStateException if a Zipkin exporter is already registered.
+ * @since 0.8
+ */
+ public static void createAndRegister(String v2Url, String serviceName) {
+ ZipkinTraceExporter.createAndRegister(v2Url, serviceName);
+ }
+
+ /**
+ * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+ * exporter can be registered at any point.
+ *
+ * @param encoder Usually {@link SpanBytesEncoder#JSON_V2}
+ * @param sender Often, but not necessarily an http sender. This could be Kafka or SQS.
+ * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+ * @throws IllegalStateException if a Zipkin exporter is already registered.
+ * @since 0.8
+ */
+ public static void createAndRegister(
+ SpanBytesEncoder encoder, Sender sender, String serviceName) {
+ ZipkinTraceExporter.createAndRegister(encoder, sender, serviceName);
+ }
+
+ /**
+ * Registers the {@code ZipkinExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ ZipkinTraceExporter.register(spanExporter, handler);
+ }
+
+ /**
+ * Unregisters the Zipkin Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Zipkin exporter is not registered.
+ * @since 0.8
+ */
+ public static void unregister() {
+ ZipkinTraceExporter.unregister();
+ }
+
+ /**
+ * Unregisters the {@code ZipkinExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ ZipkinTraceExporter.unregister(spanExporter);
+ }
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java
new file mode 100644
index 00000000..70bc725c
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandler.java
@@ -0,0 +1,215 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Scope;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.samplers.Samplers;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import zipkin2.Endpoint;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+final class ZipkinExporterHandler extends SpanExporter.Handler {
+ private static final Tracer tracer = Tracing.getTracer();
+ private static final Sampler probabilitySampler = Samplers.probabilitySampler(0.0001);
+ private static final Logger logger = Logger.getLogger(ZipkinExporterHandler.class.getName());
+
+ private static final String STATUS_CODE = "census.status_code";
+ private static final String STATUS_DESCRIPTION = "census.status_description";
+ private final SpanBytesEncoder encoder;
+ private final Sender sender;
+ private final Endpoint localEndpoint;
+
+ ZipkinExporterHandler(SpanBytesEncoder encoder, Sender sender, String serviceName) {
+ this.encoder = encoder;
+ this.sender = sender;
+ this.localEndpoint = produceLocalEndpoint(serviceName);
+ }
+
+ /** Logic borrowed from brave.internal.Platform.produceLocalEndpoint */
+ static Endpoint produceLocalEndpoint(String serviceName) {
+ Endpoint.Builder builder = Endpoint.newBuilder().serviceName(serviceName);
+ try {
+ Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
+ if (nics == null) {
+ return builder.build();
+ }
+ while (nics.hasMoreElements()) {
+ NetworkInterface nic = nics.nextElement();
+ Enumeration<InetAddress> addresses = nic.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ InetAddress address = addresses.nextElement();
+ if (address.isSiteLocalAddress()) {
+ builder.ip(address);
+ break;
+ }
+ }
+ }
+ } catch (Exception e) {
+ // don't crash the caller if there was a problem reading nics.
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "error reading nics", e);
+ }
+ }
+ return builder.build();
+ }
+
+ @SuppressWarnings("deprecation")
+ static Span generateSpan(SpanData spanData, Endpoint localEndpoint) {
+ SpanContext context = spanData.getContext();
+ long startTimestamp = toEpochMicros(spanData.getStartTimestamp());
+
+ // TODO(sebright): Fix the Checker Framework warning.
+ @SuppressWarnings("nullness")
+ long endTimestamp = toEpochMicros(spanData.getEndTimestamp());
+
+ // TODO(bdrutu): Fix the Checker Framework warning.
+ @SuppressWarnings("nullness")
+ Span.Builder spanBuilder =
+ Span.newBuilder()
+ .traceId(context.getTraceId().toLowerBase16())
+ .id(context.getSpanId().toLowerBase16())
+ .kind(toSpanKind(spanData))
+ .name(spanData.getName())
+ .timestamp(toEpochMicros(spanData.getStartTimestamp()))
+ .duration(endTimestamp - startTimestamp)
+ .localEndpoint(localEndpoint);
+
+ if (spanData.getParentSpanId() != null && spanData.getParentSpanId().isValid()) {
+ spanBuilder.parentId(spanData.getParentSpanId().toLowerBase16());
+ }
+
+ for (Map.Entry<String, AttributeValue> label :
+ spanData.getAttributes().getAttributeMap().entrySet()) {
+ spanBuilder.putTag(label.getKey(), attributeValueToString(label.getValue()));
+ }
+ Status status = spanData.getStatus();
+ if (status != null) {
+ spanBuilder.putTag(STATUS_CODE, status.getCanonicalCode().toString());
+ if (status.getDescription() != null) {
+ spanBuilder.putTag(STATUS_DESCRIPTION, status.getDescription());
+ }
+ }
+
+ for (TimedEvent<Annotation> annotation : spanData.getAnnotations().getEvents()) {
+ spanBuilder.addAnnotation(
+ toEpochMicros(annotation.getTimestamp()), annotation.getEvent().getDescription());
+ }
+
+ for (TimedEvent<io.opencensus.trace.MessageEvent> messageEvent :
+ spanData.getMessageEvents().getEvents()) {
+ spanBuilder.addAnnotation(
+ toEpochMicros(messageEvent.getTimestamp()), messageEvent.getEvent().getType().name());
+ }
+
+ return spanBuilder.build();
+ }
+
+ @javax.annotation.Nullable
+ private static Span.Kind toSpanKind(SpanData spanData) {
+ // This is a hack because the Span API did not have SpanKind.
+ if (spanData.getKind() == Kind.SERVER
+ || (spanData.getKind() == null && Boolean.TRUE.equals(spanData.getHasRemoteParent()))) {
+ return Span.Kind.SERVER;
+ }
+
+ // This is a hack because the Span API did not have SpanKind.
+ if (spanData.getKind() == Kind.CLIENT || spanData.getName().startsWith("Sent.")) {
+ return Span.Kind.CLIENT;
+ }
+
+ return null;
+ }
+
+ private static long toEpochMicros(Timestamp timestamp) {
+ return SECONDS.toMicros(timestamp.getSeconds()) + NANOSECONDS.toMicros(timestamp.getNanos());
+ }
+
+ // The return type needs to be nullable when this function is used as an argument to 'match' in
+ // attributeValueToString, because 'match' doesn't allow covariant return types.
+ private static final Function<Object, /*@Nullable*/ String> returnToString =
+ Functions.returnToString();
+
+ // TODO: Fix the Checker Framework warning.
+ @SuppressWarnings("nullness")
+ private static String attributeValueToString(AttributeValue attributeValue) {
+ return attributeValue.match(
+ returnToString,
+ returnToString,
+ returnToString,
+ returnToString,
+ Functions.<String>returnConstant(""));
+ }
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ // Start a new span with explicit 1/10000 sampling probability to avoid the case when user
+ // sets the default sampler to always sample and we get the gRPC span of the zipkin
+ // export call always sampled and go to an infinite loop.
+ Scope scope =
+ tracer.spanBuilder("SendZipkinSpans").setSampler(probabilitySampler).startScopedSpan();
+ try {
+ List<byte[]> encodedSpans = new ArrayList<byte[]>(spanDataList.size());
+ for (SpanData spanData : spanDataList) {
+ encodedSpans.add(encoder.encode(generateSpan(spanData, localEndpoint)));
+ }
+ try {
+ sender.sendSpans(encodedSpans).execute();
+ } catch (IOException e) {
+ tracer
+ .getCurrentSpan()
+ .setStatus(
+ Status.UNKNOWN.withDescription(
+ e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
+ throw new RuntimeException(e); // TODO: should we instead do drop metrics?
+ }
+ } finally {
+ scope.close();
+ }
+ }
+}
diff --git a/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java
new file mode 100644
index 00000000..aad5a563
--- /dev/null
+++ b/exporters/trace/zipkin/src/main/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporter.java
@@ -0,0 +1,124 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import zipkin2.Span;
+import zipkin2.codec.SpanBytesEncoder;
+import zipkin2.reporter.Sender;
+import zipkin2.reporter.urlconnection.URLConnectionSender;
+
+/**
+ * An OpenCensus span exporter implementation which exports data to Zipkin.
+ *
+ * <p>Example of usage:
+ *
+ * <pre>{@code
+ * public static void main(String[] args) {
+ * ZipkinTraceExporter.createAndRegister("http://127.0.0.1:9411/api/v2/spans", "myservicename");
+ * ... // Do work.
+ * }
+ * }</pre>
+ *
+ * @since 0.12
+ */
+public final class ZipkinTraceExporter {
+
+ private static final String REGISTER_NAME = ZipkinTraceExporter.class.getName();
+ private static final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ @Nullable
+ private static Handler handler = null;
+
+ private ZipkinTraceExporter() {}
+
+ /**
+ * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+ * exporter can be registered at any point.
+ *
+ * @param v2Url Ex http://127.0.0.1:9411/api/v2/spans
+ * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+ * @throws IllegalStateException if a Zipkin exporter is already registered.
+ * @since 0.12
+ */
+ public static void createAndRegister(String v2Url, String serviceName) {
+ createAndRegister(SpanBytesEncoder.JSON_V2, URLConnectionSender.create(v2Url), serviceName);
+ }
+
+ /**
+ * Creates and registers the Zipkin Trace exporter to the OpenCensus library. Only one Zipkin
+ * exporter can be registered at any point.
+ *
+ * @param encoder Usually {@link SpanBytesEncoder#JSON_V2}
+ * @param sender Often, but not necessarily an http sender. This could be Kafka or SQS.
+ * @param serviceName the {@link Span#localServiceName() local service name} of the process.
+ * @throws IllegalStateException if a Zipkin exporter is already registered.
+ * @since 0.12
+ */
+ public static void createAndRegister(
+ SpanBytesEncoder encoder, Sender sender, String serviceName) {
+ synchronized (monitor) {
+ checkState(handler == null, "Zipkin exporter is already registered.");
+ Handler newHandler = new ZipkinExporterHandler(encoder, sender, serviceName);
+ handler = newHandler;
+ register(Tracing.getExportComponent().getSpanExporter(), newHandler);
+ }
+ }
+
+ /**
+ * Registers the {@code ZipkinTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} where this service is registered.
+ */
+ @VisibleForTesting
+ static void register(SpanExporter spanExporter, Handler handler) {
+ spanExporter.registerHandler(REGISTER_NAME, handler);
+ }
+
+ /**
+ * Unregisters the Zipkin Trace exporter from the OpenCensus library.
+ *
+ * @throws IllegalStateException if a Zipkin exporter is not registered.
+ * @since 0.12
+ */
+ public static void unregister() {
+ synchronized (monitor) {
+ checkState(handler != null, "Zipkin exporter is not registered.");
+ unregister(Tracing.getExportComponent().getSpanExporter());
+ handler = null;
+ }
+ }
+
+ /**
+ * Unregisters the {@code ZipkinTraceExporter}.
+ *
+ * @param spanExporter the instance of the {@code SpanExporter} from where this service is
+ * unregistered.
+ */
+ @VisibleForTesting
+ static void unregister(SpanExporter spanExporter) {
+ spanExporter.unregisterHandler(REGISTER_NAME);
+ }
+}
diff --git a/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java
new file mode 100644
index 00000000..7e293003
--- /dev/null
+++ b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinExporterHandlerTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import io.opencensus.common.Timestamp;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.MessageEvent.Type;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.Attributes;
+import io.opencensus.trace.export.SpanData.Links;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import io.opencensus.trace.export.SpanData.TimedEvents;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import zipkin2.Endpoint;
+import zipkin2.Span;
+
+/** Unit tests for {@link ZipkinExporterHandler}. */
+@RunWith(JUnit4.class)
+public class ZipkinExporterHandlerTest {
+ private static final Endpoint localEndpoint =
+ Endpoint.newBuilder().serviceName("tweetiebird").build();
+ private static final String TRACE_ID = "d239036e7d5cec116b562147388b35bf";
+ private static final String SPAN_ID = "9cc1e3049173be09";
+ private static final String PARENT_SPAN_ID = "8b03ab423da481c5";
+ private static final Map<String, AttributeValue> attributes = Collections.emptyMap();
+ private static final List<TimedEvent<Annotation>> annotations = Collections.emptyList();
+ private static final List<TimedEvent<MessageEvent>> messageEvents =
+ ImmutableList.of(
+ TimedEvent.create(
+ Timestamp.create(1505855799, 433901068),
+ MessageEvent.builder(Type.RECEIVED, 0).setCompressedMessageSize(7).build()),
+ TimedEvent.create(
+ Timestamp.create(1505855799, 459486280),
+ MessageEvent.builder(Type.SENT, 0).setCompressedMessageSize(13).build()));
+
+ @Test
+ public void generateSpan_NoKindAndRemoteParent() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ // TODO SpanId.fromLowerBase16
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "Recv.helloworld.Greeter.SayHello", /* name */
+ null, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+ .isEqualTo(
+ Span.newBuilder()
+ .traceId(TRACE_ID)
+ .parentId(PARENT_SPAN_ID)
+ .id(SPAN_ID)
+ .kind(Span.Kind.SERVER)
+ .name(data.getName())
+ .timestamp(1505855794000000L + 194009601L / 1000)
+ .duration(
+ (1505855799000000L + 465726528L / 1000)
+ - (1505855794000000L + 194009601L / 1000))
+ .localEndpoint(localEndpoint)
+ .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+ .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+ .putTag("census.status_code", "OK")
+ .build());
+ }
+
+ @Test
+ public void generateSpan_ServerKind() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ // TODO SpanId.fromLowerBase16
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "Recv.helloworld.Greeter.SayHello", /* name */
+ Kind.SERVER, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+ .isEqualTo(
+ Span.newBuilder()
+ .traceId(TRACE_ID)
+ .parentId(PARENT_SPAN_ID)
+ .id(SPAN_ID)
+ .kind(Span.Kind.SERVER)
+ .name(data.getName())
+ .timestamp(1505855794000000L + 194009601L / 1000)
+ .duration(
+ (1505855799000000L + 465726528L / 1000)
+ - (1505855794000000L + 194009601L / 1000))
+ .localEndpoint(localEndpoint)
+ .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+ .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+ .putTag("census.status_code", "OK")
+ .build());
+ }
+
+ @Test
+ public void generateSpan_ClientKind() {
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ // TODO SpanId.fromLowerBase16
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "Sent.helloworld.Greeter.SayHello", /* name */
+ Kind.CLIENT, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributes, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+ .isEqualTo(
+ Span.newBuilder()
+ .traceId(TRACE_ID)
+ .parentId(PARENT_SPAN_ID)
+ .id(SPAN_ID)
+ .kind(Span.Kind.CLIENT)
+ .name(data.getName())
+ .timestamp(1505855794000000L + 194009601L / 1000)
+ .duration(
+ (1505855799000000L + 465726528L / 1000)
+ - (1505855794000000L + 194009601L / 1000))
+ .localEndpoint(localEndpoint)
+ .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+ .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+ .putTag("census.status_code", "OK")
+ .build());
+ }
+
+ @Test
+ public void generateSpan_WithAttributes() {
+ Map<String, AttributeValue> attributeMap = new HashMap<String, AttributeValue>();
+ attributeMap.put("string", AttributeValue.stringAttributeValue("string value"));
+ attributeMap.put("boolean", AttributeValue.booleanAttributeValue(false));
+ attributeMap.put("long", AttributeValue.longAttributeValue(9999L));
+ SpanData data =
+ SpanData.create(
+ SpanContext.create(
+ TraceId.fromLowerBase16(TRACE_ID),
+ SpanId.fromLowerBase16(SPAN_ID),
+ TraceOptions.builder().setIsSampled(true).build()),
+ // TODO SpanId.fromLowerBase16
+ SpanId.fromLowerBase16(PARENT_SPAN_ID),
+ true, /* hasRemoteParent */
+ "Sent.helloworld.Greeter.SayHello", /* name */
+ Kind.CLIENT, /* kind */
+ Timestamp.create(1505855794, 194009601) /* startTimestamp */,
+ Attributes.create(attributeMap, 0 /* droppedAttributesCount */),
+ TimedEvents.create(annotations, 0 /* droppedEventsCount */),
+ TimedEvents.create(messageEvents, 0 /* droppedEventsCount */),
+ Links.create(Collections.<Link>emptyList(), 0 /* droppedLinksCount */),
+ null, /* childSpanCount */
+ Status.OK,
+ Timestamp.create(1505855799, 465726528) /* endTimestamp */);
+
+ assertThat(ZipkinExporterHandler.generateSpan(data, localEndpoint))
+ .isEqualTo(
+ Span.newBuilder()
+ .traceId(TRACE_ID)
+ .parentId(PARENT_SPAN_ID)
+ .id(SPAN_ID)
+ .kind(Span.Kind.CLIENT)
+ .name(data.getName())
+ .timestamp(1505855794000000L + 194009601L / 1000)
+ .duration(
+ (1505855799000000L + 465726528L / 1000)
+ - (1505855794000000L + 194009601L / 1000))
+ .localEndpoint(localEndpoint)
+ .addAnnotation(1505855799000000L + 433901068L / 1000, "RECEIVED")
+ .addAnnotation(1505855799000000L + 459486280L / 1000, "SENT")
+ .putTag("census.status_code", "OK")
+ .putTag("string", "string value")
+ .putTag("boolean", "false")
+ .putTag("long", "9999")
+ .build());
+ }
+}
diff --git a/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java
new file mode 100644
index 00000000..2a032d0f
--- /dev/null
+++ b/exporters/trace/zipkin/src/test/java/io/opencensus/exporter/trace/zipkin/ZipkinTraceExporterTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.exporter.trace.zipkin;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.verify;
+
+import io.opencensus.trace.export.SpanExporter;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link ZipkinTraceExporter}. */
+@RunWith(JUnit4.class)
+public class ZipkinTraceExporterTest {
+ @Mock private SpanExporter spanExporter;
+ @Mock private Handler handler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void registerUnregisterZipkinExporter() {
+ ZipkinTraceExporter.register(spanExporter, handler);
+ verify(spanExporter)
+ .registerHandler(
+ eq("io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter"), same(handler));
+ ZipkinTraceExporter.unregister(spanExporter);
+ verify(spanExporter)
+ .unregisterHandler(eq("io.opencensus.exporter.trace.zipkin.ZipkinTraceExporter"));
+ }
+}
diff --git a/findbugs-exclude.xml b/findbugs-exclude.xml
new file mode 100644
index 00000000..014f9a9b
--- /dev/null
+++ b/findbugs-exclude.xml
@@ -0,0 +1,106 @@
+<FindBugsFilter>
+ <Match>
+ <!-- Reason: Null has a different meaning than a zero-length array in this case. -->
+ <Bug pattern="PZLA_PREFER_ZERO_LENGTH_ARRAYS"/>
+ <Class name="io.opencensus.stats.MutableDistribution"/>
+ <Method name="getInternalBucketCountsArray"/>
+ </Match>
+ <Match>
+ <!-- Reason: Equal is implemented in the AutoValue generated class. -->
+ <Bug pattern="EQ_COMPARETO_USE_OBJECT_EQUALS"/>
+ <Class name="io.opencensus.common.Timestamp"/>
+ <Method name="compareTo"/>
+ </Match>
+ <Match>
+ <!-- Reason: Equal is implemented in the AutoValue generated class. -->
+ <Bug pattern="EQ_COMPARETO_USE_OBJECT_EQUALS"/>
+ <Class name="io.opencensus.common.Duration"/>
+ <Method name="compareTo"/>
+ </Match>
+ <Match>
+ <!-- Reason: BaseMessageEvent only has two visible subclasses. -->
+ <Bug pattern="BC_UNCONFIRMED_CAST"/>
+ <Class name="io.opencensus.trace.internal.BaseMessageEventUtils"/>
+ </Match>
+ <Match>
+ <!-- Reason: This test is testing for a NPE. -->
+ <Bug pattern="NP_NONNULL_PARAM_VIOLATION"/>
+ <Class name="io.opencensus.internal.UtilsTest"/>
+ <Method name="checkNotNull"/>
+ </Match>
+ <Match>
+ <!-- Reason: This test is testing for a NPE. -->
+ <Bug pattern="NP_NONNULL_PARAM_VIOLATION"/>
+ <Class name="io.opencensus.internal.UtilsTest"/>
+ <Method name="checkNotNull_NullErrorMessage"/>
+ </Match>
+ <Match>
+ <!-- Reason: It seems like FindBugs incorrectly assumes that all -->
+ <!-- Throwables are subclasses of Error or Exception. -->
+ <Bug pattern="BC_VACUOUS_INSTANCEOF"/>
+ <Class name="io.opencensus.trace.CurrentSpanUtils$CallableInSpan"/>
+ <Method name="call"/>
+ </Match>
+ <Match>
+ <!-- Reason: Protobuf auto-generated code. -->
+ <Bug pattern="UCF_USELESS_CONTROL_FLOW"/>
+ <Class name="io.opencensus.contrib.appengine.standard.util.TraceIdProto$Builder"/>
+ <Method name="maybeForceBuilderInitialization"/>
+ </Match>
+ <Match>
+ <!-- Reason: The synchronization in the setState is for the side effects not for the state. -->
+ <Bug pattern="UG_SYNC_SET_UNSYNC_GET"/>
+ <Class name="io.opencensus.implcore.stats.StatsComponentImplBase"/>
+ </Match>
+
+ <!-- Suppress some FindBugs warnings related to performance or robustness -->
+ <!-- in test classes, where those issues are less important. -->
+ <Match>
+ <!-- Reason: Only needed for performance. -->
+ <Bug pattern="SIC_INNER_SHOULD_BE_STATIC_ANON"/>
+ <Source name="~.*Test\.java"/>
+ </Match>
+ <Match>
+ <!-- Reason: Only needed for performance. -->
+ <Bug pattern="WMI_WRONG_MAP_ITERATOR"/>
+ <Source name="~.*Test\.java"/>
+ </Match>
+ <Match>
+ <!-- Reason: Only needed for performance. -->
+ <Bug pattern="UM_UNNECESSARY_MATH"/>
+ <Source name="~.*Test\.java"/>
+ </Match>
+ <Match>
+ <!-- Reason: This is less important in a test environment. -->
+ <Bug pattern="DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED"/>
+ <Source name="~.*Test\.java"/>
+ </Match>
+ <Match>
+ <!-- Reason: Many classes initialize fields in @Before methods. -->
+ <Bug pattern="UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"/>
+ <Source name="~.*Test\.java"/>
+ </Match>
+
+ <!-- Suppress all FindBugs warnings about NullPointerExceptions in -->
+ <!-- non-test code. They are redundant with the Checker Framework's -->
+ <!-- warnings, and they sometimes conflict. These warnings are still -->
+ <!-- useful in test code, where we don't use the Checker Framework. -->
+ <Match>
+ <Bug code="NP"/>
+ <Not>
+ <Source name="~.*Test\.java"/>
+ </Not>
+ </Match>
+ <Match>
+ <Bug pattern="UR_UNINIT_READ"/>
+ <Not>
+ <Source name="~.*Test\.java"/>
+ </Not>
+ </Match>
+ <Match>
+ <Bug pattern="UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"/>
+ <Not>
+ <Source name="~.*Test\.java"/>
+ </Not>
+ </Match>
+</FindBugsFilter>
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..758de960
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..a95009c3
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..cccdd3d5
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..e95643d6
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/impl/README.md b/impl/README.md
new file mode 100644
index 00000000..3dee26f9
--- /dev/null
+++ b/impl/README.md
@@ -0,0 +1,5 @@
+OpenCensus Java implementation
+======================================================
+
+* Java 7 compatible.
+* Contains any classes not compatible with Android.
diff --git a/impl/build.gradle b/impl/build.gradle
new file mode 100644
index 00000000..6dacdddb
--- /dev/null
+++ b/impl/build.gradle
@@ -0,0 +1,21 @@
+description = 'OpenCensus Implementation'
+
+apply plugin: 'java'
+
+[compileJava, compileTestJava].each() {
+ it.sourceCompatibility = 1.7
+ it.targetCompatibility = 1.7
+}
+
+dependencies {
+ compile project(':opencensus-api'),
+ project(':opencensus-impl-core'),
+ libraries.disruptor
+
+ testCompile project(':opencensus-api'),
+ project(':opencensus-impl-core')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
+
+javadoc.exclude 'io/opencensus/internal/**' \ No newline at end of file
diff --git a/impl/src/main/java/io/opencensus/impl/internal/DisruptorEventQueue.java b/impl/src/main/java/io/opencensus/impl/internal/DisruptorEventQueue.java
new file mode 100644
index 00000000..a0445b53
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/internal/DisruptorEventQueue.java
@@ -0,0 +1,241 @@
+/*
+ * 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.impl.internal;
+
+import com.lmax.disruptor.EventFactory;
+import com.lmax.disruptor.EventHandler;
+import com.lmax.disruptor.RingBuffer;
+import com.lmax.disruptor.SleepingWaitStrategy;
+import com.lmax.disruptor.dsl.Disruptor;
+import com.lmax.disruptor.dsl.ProducerType;
+import io.opencensus.implcore.internal.DaemonThreadFactory;
+import io.opencensus.implcore.internal.EventQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A low-latency event queue for background updating of (possibly contended) objects. This is
+ * intended for use by instrumentation methods to ensure that they do not block foreground
+ * activities. To customize the action taken on reading the queue, derive a new class from {@link
+ * EventQueue.Entry} and pass it to the {@link #enqueue(Entry)} method. The {@link Entry#process()}
+ * method of your class will be called and executed in a background thread. This class is a
+ * Singleton.
+ *
+ * <p>Example Usage: Given a class as follows:
+ *
+ * <pre>
+ * public class someClass {
+ * public void doSomething() {
+ * // Do the work of the method. One result is a measurement of something.
+ * int measurement = doSomeWork();
+ * // Make an update to the class state, based on this measurement. This work can take some
+ * // time, but can be done asynchronously, in the background.
+ * update(measurement);
+ * }
+ *
+ * public void update(int arg) {
+ * // do something
+ * }
+ * }
+ * </pre>
+ *
+ * <p>The work of calling {@code someClass.update()} can be executed in the backgound as follows:
+ *
+ * <pre>
+ * public class someClass {
+ * // Add a EventQueueEntry class that will process the update call.
+ * private static final class SomeClassUpdateEvent implements EventQueueEntry {
+ * private final SomeClass someClassInstance;
+ * private final int arg;
+ *
+ * SomeObjectUpdateEvent(SomeObject someClassInstance, int arg) {
+ * this.someClassInstance = someClassInstance;
+ * this.arg = arg;
+ * }
+ *
+ * &#064;Override
+ * public void process() {
+ * someClassInstance.update(arg);
+ * }
+ * }
+ *
+ * public void doSomething() {
+ * int measurement = doSomeWork();
+ * // Instead of calling update() directly, create an event to do the processing, and insert
+ * // it into the EventQueue. It will be processed in a background thread, and doSomething()
+ * // can return immediately.
+ * EventQueue.getInstance.enqueue(new SomeClassUpdateEvent(this, measurement));
+ * }
+ * }
+ * </pre>
+ */
+@ThreadSafe
+public final class DisruptorEventQueue implements EventQueue {
+
+ private static final Logger logger = Logger.getLogger(DisruptorEventQueue.class.getName());
+
+ // Number of events that can be enqueued at any one time. If more than this are enqueued,
+ // then subsequent attempts to enqueue new entries will block.
+ // TODO(aveitch): consider making this a parameter to the constructor, so the queue can be
+ // configured to a size appropriate to the system (smaller/less busy systems will not need as
+ // large a queue.
+ private static final int DISRUPTOR_BUFFER_SIZE = 8192;
+ // The single instance of the class.
+ private static final DisruptorEventQueue eventQueue = create();
+
+ // The event queue is built on this {@link Disruptor}.
+ private final Disruptor<DisruptorEvent> disruptor;
+ // Ring Buffer for the {@link Disruptor} that underlies the queue.
+ private final RingBuffer<DisruptorEvent> ringBuffer;
+
+ private volatile DisruptorEnqueuer enqueuer;
+
+ // Creates a new EventQueue. Private to prevent creation of non-singleton instance.
+ private DisruptorEventQueue(
+ Disruptor<DisruptorEvent> disruptor,
+ RingBuffer<DisruptorEvent> ringBuffer,
+ DisruptorEnqueuer enqueuer) {
+ this.disruptor = disruptor;
+ this.ringBuffer = ringBuffer;
+ this.enqueuer = enqueuer;
+ }
+
+ // Creates a new EventQueue. Private to prevent creation of non-singleton instance.
+ private static DisruptorEventQueue create() {
+ // Create new Disruptor for processing. Note that Disruptor creates a single thread per
+ // consumer (see https://github.com/LMAX-Exchange/disruptor/issues/121 for details);
+ // this ensures that the event handler can take unsynchronized actions whenever possible.
+ Disruptor<DisruptorEvent> disruptor =
+ new Disruptor<>(
+ DisruptorEventFactory.INSTANCE,
+ DISRUPTOR_BUFFER_SIZE,
+ new DaemonThreadFactory("OpenCensus.Disruptor"),
+ ProducerType.MULTI,
+ new SleepingWaitStrategy());
+ disruptor.handleEventsWith(new DisruptorEventHandler[] {DisruptorEventHandler.INSTANCE});
+ disruptor.start();
+ final RingBuffer<DisruptorEvent> ringBuffer = disruptor.getRingBuffer();
+
+ DisruptorEnqueuer enqueuer =
+ new DisruptorEnqueuer() {
+ @Override
+ public void enqueue(Entry entry) {
+ long sequence = ringBuffer.next();
+ try {
+ DisruptorEvent event = ringBuffer.get(sequence);
+ event.setEntry(entry);
+ } finally {
+ ringBuffer.publish(sequence);
+ }
+ }
+ };
+ return new DisruptorEventQueue(disruptor, ringBuffer, enqueuer);
+ }
+
+ /**
+ * Returns the {@link DisruptorEventQueue} instance.
+ *
+ * @return the singleton {@code EventQueue} instance.
+ */
+ public static DisruptorEventQueue getInstance() {
+ return eventQueue;
+ }
+
+ /**
+ * Enqueues an event on the {@link DisruptorEventQueue}.
+ *
+ * @param entry a class encapsulating the actions to be taken for event processing.
+ */
+ @Override
+ public void enqueue(Entry entry) {
+ enqueuer.enqueue(entry);
+ }
+
+ /** Shuts down the underlying disruptor. */
+ @Override
+ public void shutdown() {
+ enqueuer =
+ new DisruptorEnqueuer() {
+ final AtomicBoolean logged = new AtomicBoolean(false);
+
+ @Override
+ public void enqueue(Entry entry) {
+ if (!logged.getAndSet(true)) {
+ logger.log(Level.INFO, "Attempted to enqueue entry after Disruptor shutdown.");
+ }
+ }
+ };
+
+ disruptor.shutdown();
+ }
+
+ // Allows this event queue to safely shutdown by not enqueuing events on the ring buffer
+ private abstract static class DisruptorEnqueuer {
+
+ public abstract void enqueue(Entry entry);
+ }
+
+ // An event in the {@link EventQueue}. Just holds a reference to an EventQueue.Entry.
+ private static final class DisruptorEvent {
+
+ // TODO(bdrutu): Investigate if volatile is needed. This object is shared between threads so
+ // intuitively this variable must be volatile.
+ @Nullable private volatile Entry entry = null;
+
+ // Sets the EventQueueEntry associated with this DisruptorEvent.
+ void setEntry(@Nullable Entry entry) {
+ this.entry = entry;
+ }
+
+ // Returns the EventQueueEntry associated with this DisruptorEvent.
+ @Nullable
+ Entry getEntry() {
+ return entry;
+ }
+ }
+
+ // Factory for DisruptorEvent.
+ private enum DisruptorEventFactory implements EventFactory<DisruptorEvent> {
+ INSTANCE;
+
+ @Override
+ public DisruptorEvent newInstance() {
+ return new DisruptorEvent();
+ }
+ }
+
+ /**
+ * Every event that gets added to {@link EventQueue} will get processed here. Just calls the
+ * underlying process() method.
+ */
+ private enum DisruptorEventHandler implements EventHandler<DisruptorEvent> {
+ INSTANCE;
+
+ @Override
+ public void onEvent(DisruptorEvent event, long sequence, boolean endOfBatch) {
+ Entry entry = event.getEntry();
+ if (entry != null) {
+ entry.process();
+ }
+ // Remove the reference to the previous entry to allow the memory to be gc'ed.
+ event.setEntry(null);
+ }
+ }
+}
diff --git a/impl/src/main/java/io/opencensus/impl/metrics/MetricsComponentImpl.java b/impl/src/main/java/io/opencensus/impl/metrics/MetricsComponentImpl.java
new file mode 100644
index 00000000..53c354f1
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/metrics/MetricsComponentImpl.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018, 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.impl.metrics;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.metrics.MetricsComponentImplBase;
+import io.opencensus.metrics.MetricsComponent;
+
+/** Implementation of {@link MetricsComponent}. */
+public final class MetricsComponentImpl extends MetricsComponentImplBase {
+
+ public MetricsComponentImpl() {
+ super(MillisClock.getInstance());
+ }
+}
diff --git a/impl/src/main/java/io/opencensus/impl/stats/StatsComponentImpl.java b/impl/src/main/java/io/opencensus/impl/stats/StatsComponentImpl.java
new file mode 100644
index 00000000..6b9fe69a
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/stats/StatsComponentImpl.java
@@ -0,0 +1,31 @@
+/*
+ * 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.impl.stats;
+
+import io.opencensus.impl.internal.DisruptorEventQueue;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.stats.StatsComponentImplBase;
+import io.opencensus.stats.StatsComponent;
+
+/** Java 7 and 8 implementation of {@link StatsComponent}. */
+public final class StatsComponentImpl extends StatsComponentImplBase {
+
+ /** Public constructor to be used with reflection loading. */
+ public StatsComponentImpl() {
+ super(DisruptorEventQueue.getInstance(), MillisClock.getInstance());
+ }
+}
diff --git a/impl/src/main/java/io/opencensus/impl/tags/TagsComponentImpl.java b/impl/src/main/java/io/opencensus/impl/tags/TagsComponentImpl.java
new file mode 100644
index 00000000..8dd1f373
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/tags/TagsComponentImpl.java
@@ -0,0 +1,23 @@
+/*
+ * 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.impl.tags;
+
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.TagsComponent;
+
+/** Java 7 and 8 implementation of {@link TagsComponent}. */
+public final class TagsComponentImpl extends TagsComponentImplBase {}
diff --git a/impl/src/main/java/io/opencensus/impl/trace/TraceComponentImpl.java b/impl/src/main/java/io/opencensus/impl/trace/TraceComponentImpl.java
new file mode 100644
index 00000000..1cd70023
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/trace/TraceComponentImpl.java
@@ -0,0 +1,67 @@
+/*
+ * 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.impl.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.impl.internal.DisruptorEventQueue;
+import io.opencensus.impl.trace.internal.ThreadLocalRandomHandler;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.trace.TraceComponentImplBase;
+import io.opencensus.trace.TraceComponent;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/** Java 7 and 8 implementation of the {@link TraceComponent}. */
+public final class TraceComponentImpl extends TraceComponent {
+ private final TraceComponentImplBase traceComponentImplBase;
+
+ /** Public constructor to be used with reflection loading. */
+ public TraceComponentImpl() {
+ traceComponentImplBase =
+ new TraceComponentImplBase(
+ MillisClock.getInstance(),
+ new ThreadLocalRandomHandler(),
+ DisruptorEventQueue.getInstance());
+ }
+
+ @Override
+ public Tracer getTracer() {
+ return traceComponentImplBase.getTracer();
+ }
+
+ @Override
+ public PropagationComponent getPropagationComponent() {
+ return traceComponentImplBase.getPropagationComponent();
+ }
+
+ @Override
+ public Clock getClock() {
+ return traceComponentImplBase.getClock();
+ }
+
+ @Override
+ public ExportComponent getExportComponent() {
+ return traceComponentImplBase.getExportComponent();
+ }
+
+ @Override
+ public TraceConfig getTraceConfig() {
+ return traceComponentImplBase.getTraceConfig();
+ }
+}
diff --git a/impl/src/main/java/io/opencensus/impl/trace/internal/ThreadLocalRandomHandler.java b/impl/src/main/java/io/opencensus/impl/trace/internal/ThreadLocalRandomHandler.java
new file mode 100644
index 00000000..d13e3982
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/impl/trace/internal/ThreadLocalRandomHandler.java
@@ -0,0 +1,35 @@
+/*
+ * 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.impl.trace.internal;
+
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Implementation of the {@link RandomHandler} using {@link ThreadLocalRandom}. */
+@ThreadSafe
+public final class ThreadLocalRandomHandler extends RandomHandler {
+
+ /** Constructs a new {@code ThreadLocalRandomHandler}. */
+ public ThreadLocalRandomHandler() {}
+
+ @Override
+ public Random current() {
+ return ThreadLocalRandom.current();
+ }
+}
diff --git a/impl/src/main/java/io/opencensus/trace/TraceComponentImpl.java b/impl/src/main/java/io/opencensus/trace/TraceComponentImpl.java
new file mode 100644
index 00000000..76da3bd0
--- /dev/null
+++ b/impl/src/main/java/io/opencensus/trace/TraceComponentImpl.java
@@ -0,0 +1,68 @@
+/*
+ * 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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.impl.internal.DisruptorEventQueue;
+import io.opencensus.impl.trace.internal.ThreadLocalRandomHandler;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.trace.TraceComponentImplBase;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/** Java 7 and 8 implementation of the {@link TraceComponent}. */
+// TraceComponentImpl was moved to io.opencensus.impl.trace. This class exists for backwards
+// compatibility, so that it can be loaded by opencensus-api 0.5.
+@Deprecated
+public final class TraceComponentImpl extends TraceComponent {
+ private final TraceComponentImplBase traceComponentImplBase;
+
+ /** Public constructor to be used with reflection loading. */
+ public TraceComponentImpl() {
+ traceComponentImplBase =
+ new TraceComponentImplBase(
+ MillisClock.getInstance(),
+ new ThreadLocalRandomHandler(),
+ DisruptorEventQueue.getInstance());
+ }
+
+ @Override
+ public Tracer getTracer() {
+ return traceComponentImplBase.getTracer();
+ }
+
+ @Override
+ public PropagationComponent getPropagationComponent() {
+ return traceComponentImplBase.getPropagationComponent();
+ }
+
+ @Override
+ public Clock getClock() {
+ return traceComponentImplBase.getClock();
+ }
+
+ @Override
+ public ExportComponent getExportComponent() {
+ return traceComponentImplBase.getExportComponent();
+ }
+
+ @Override
+ public TraceConfig getTraceConfig() {
+ return traceComponentImplBase.getTraceConfig();
+ }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/internal/DisruptorEventQueueTest.java b/impl/src/test/java/io/opencensus/impl/internal/DisruptorEventQueueTest.java
new file mode 100644
index 00000000..f12498fa
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/internal/DisruptorEventQueueTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.impl.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.internal.EventQueue;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DisruptorEventQueue}. */
+@RunWith(JUnit4.class)
+public class DisruptorEventQueueTest {
+ // Simple class to use that keeps an incrementing counter. Will fail with an assertion if
+ // increment is used from multiple threads, or if the stored value is different from that expected
+ // by the caller.
+ private static class Counter {
+ private int count;
+ private volatile long id; // stores thread ID used in first increment operation.
+
+ public Counter() {
+ count = 0;
+ id = -1;
+ }
+
+ // Increments counter by 1. Will fail in assertion if multiple different threads are used
+ // (the EventQueue backend should be single-threaded).
+ public void increment() {
+ long tid = Thread.currentThread().getId();
+ if (id == -1) {
+ assertThat(count).isEqualTo(0);
+ id = tid;
+ } else {
+ assertThat(id).isEqualTo(tid);
+ }
+ count++;
+ }
+
+ // Check the current value of the counter. Assert if it is not the expected value.
+ public void check(int value) {
+ assertThat(count).isEqualTo(value);
+ }
+ }
+
+ // EventQueueEntry for incrementing a Counter.
+ private static class IncrementEvent implements EventQueue.Entry {
+ private final Counter counter;
+
+ IncrementEvent(Counter counter) {
+ this.counter = counter;
+ }
+
+ @Override
+ public void process() {
+ counter.increment();
+ }
+ }
+
+ @Test
+ public void incrementOnce() {
+ Counter counter = new Counter();
+ IncrementEvent ie = new IncrementEvent(counter);
+ DisruptorEventQueue.getInstance().enqueue(ie);
+ // Sleep briefly, to allow background operations to complete.
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ counter.check(1);
+ }
+
+ @Test
+ public void incrementTenK() {
+ final int tenK = 10000;
+ Counter counter = new Counter();
+ for (int i = 0; i < tenK; i++) {
+ IncrementEvent ie = new IncrementEvent(counter);
+ DisruptorEventQueue.getInstance().enqueue(ie);
+ }
+ // Sleep briefly, to allow background operations to complete.
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ counter.check(tenK);
+ }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/metrics/MetricsTest.java b/impl/src/test/java/io/opencensus/impl/metrics/MetricsTest.java
new file mode 100644
index 00000000..439933de
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/metrics/MetricsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018, 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.impl.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.metrics.MetricRegistryImpl;
+import io.opencensus.implcore.metrics.export.ExportComponentImpl;
+import io.opencensus.metrics.Metrics;
+import io.opencensus.metrics.MetricsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link MetricsComponent} through the {@link Metrics} class. */
+@RunWith(JUnit4.class)
+public class MetricsTest {
+
+ @Test
+ public void getExportComponent() {
+ assertThat(Metrics.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+ }
+
+ @Test
+ public void getMetricRegistry() {
+ assertThat(Metrics.getMetricRegistry()).isInstanceOf(MetricRegistryImpl.class);
+ }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/stats/StatsTest.java b/impl/src/test/java/io/opencensus/impl/stats/StatsTest.java
new file mode 100644
index 00000000..23606a48
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/stats/StatsTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.impl.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.stats.StatsRecorderImpl;
+import io.opencensus.implcore.stats.ViewManagerImpl;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link StatsComponent} through the {@link Stats} class. */
+@RunWith(JUnit4.class)
+public final class StatsTest {
+ @Test
+ public void getStatsRecorder() {
+ assertThat(Stats.getStatsRecorder()).isInstanceOf(StatsRecorderImpl.class);
+ }
+
+ @Test
+ public void getViewManager() {
+ assertThat(Stats.getViewManager()).isInstanceOf(ViewManagerImpl.class);
+ }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/tags/TagsTest.java b/impl/src/test/java/io/opencensus/impl/tags/TagsTest.java
new file mode 100644
index 00000000..e94cf254
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/tags/TagsTest.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.impl.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.tags.TaggerImpl;
+import io.opencensus.implcore.tags.propagation.TagPropagationComponentImpl;
+import io.opencensus.tags.Tags;
+import io.opencensus.tags.TagsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link TagsComponent} through the {@link Tags} class. */
+@RunWith(JUnit4.class)
+public final class TagsTest {
+ @Test
+ public void getTagger() {
+ assertThat(Tags.getTagger()).isInstanceOf(TaggerImpl.class);
+ }
+
+ @Test
+ public void getTagContextSerializer() {
+ assertThat(Tags.getTagPropagationComponent()).isInstanceOf(TagPropagationComponentImpl.class);
+ }
+}
diff --git a/impl/src/test/java/io/opencensus/impl/trace/TracingTest.java b/impl/src/test/java/io/opencensus/impl/trace/TracingTest.java
new file mode 100644
index 00000000..e58ce1cb
--- /dev/null
+++ b/impl/src/test/java/io/opencensus/impl/trace/TracingTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.impl.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.trace.TracerImpl;
+import io.opencensus.implcore.trace.export.ExportComponentImpl;
+import io.opencensus.trace.TraceComponent;
+import io.opencensus.trace.Tracing;
+import io.opencensus.trace.propagation.PropagationComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link TraceComponent} through the {@link Tracing} class. */
+@RunWith(JUnit4.class)
+public class TracingTest {
+ @Test
+ public void implementationOfTracer() {
+ assertThat(Tracing.getTracer()).isInstanceOf(TracerImpl.class);
+ }
+
+ @Test
+ public void implementationOfBinaryPropagationHandler() {
+ assertThat(Tracing.getPropagationComponent()).isInstanceOf(PropagationComponent.class);
+ }
+
+ @Test
+ public void implementationOfClock() {
+ assertThat(Tracing.getClock()).isInstanceOf(MillisClock.class);
+ }
+
+ @Test
+ public void implementationOfTraceExporter() {
+ assertThat(Tracing.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+ }
+}
diff --git a/impl_core/README.md b/impl_core/README.md
new file mode 100644
index 00000000..901177c8
--- /dev/null
+++ b/impl_core/README.md
@@ -0,0 +1,5 @@
+OpenCensus implementation
+======================================================
+
+* The main implementation shared between Java and Android.
+* Java 7 and Android compatible.
diff --git a/impl_core/build.gradle b/impl_core/build.gradle
new file mode 100644
index 00000000..21158c36
--- /dev/null
+++ b/impl_core/build.gradle
@@ -0,0 +1,17 @@
+description = 'OpenCensus Core Implementation'
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ compileOnly libraries.auto_value
+
+ testCompile project(':opencensus-api'),
+ project(':opencensus-testing')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
+
+javadoc.exclude 'io/opencensus/internal/**'
+javadoc.exclude 'io/opencensus/trace/internal/**' \ No newline at end of file
diff --git a/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/B3FormatImplBenchmark.java b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/B3FormatImplBenchmark.java
new file mode 100644
index 00000000..736c3705
--- /dev/null
+++ b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/B3FormatImplBenchmark.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2018, 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.trace.propagation;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+
+/** Benchmarks for {@link io.opencensus.implcore.trace.propagation.B3Format}. */
+@State(Scope.Benchmark)
+public class B3FormatImplBenchmark {
+ @State(Scope.Thread)
+ public static class Data {
+ private TextFormatBenchmarkBase textFormatBase;
+ private SpanContext spanContext;
+ private Map<String, String> spanContextHeaders;
+
+ @Setup
+ public void setup() {
+ textFormatBase = new TextFormatBenchmarkBase(new B3Format());
+ Random random = new Random(1234);
+ spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ TraceOptions.builder().setIsSampled(random.nextBoolean()).build(),
+ Tracestate.builder().build());
+ spanContextHeaders = new HashMap<String, String>();
+ textFormatBase.inject(spanContext, spanContextHeaders);
+ }
+ }
+
+ /**
+ * This benchmark attempts to measure performance of {@link TextFormat#inject(SpanContext, Object,
+ * Setter)}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public Map<String, String> inject(Data data) {
+ Map<String, String> carrier = new HashMap<String, String>();
+ data.textFormatBase.inject(data.spanContext, carrier);
+ return carrier;
+ }
+
+ /**
+ * This benchmark attempts to measure performance of {@link TextFormat#extract(Object, Getter)}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public SpanContext extract(Data data) throws SpanContextParseException {
+ return data.textFormatBase.extract(data.spanContextHeaders);
+ }
+
+ /**
+ * This benchmark attempts to measure performance of {@link TextFormat#inject(SpanContext, Object,
+ * Setter)} then {@link TextFormat#extract(Object, Getter)}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public SpanContext injectExtract(Data data) throws SpanContextParseException {
+ Map<String, String> carrier = new HashMap<String, String>();
+ data.textFormatBase.inject(data.spanContext, carrier);
+ return data.textFormatBase.extract(carrier);
+ }
+}
diff --git a/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplBenchmark.java b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplBenchmark.java
new file mode 100644
index 00000000..70e590bf
--- /dev/null
+++ b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplBenchmark.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.trace.propagation;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.BinaryFormat;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+
+/** Benchmarks for {@link BinaryFormat}. */
+@State(Scope.Benchmark)
+public class BinaryFormatImplBenchmark {
+ @State(Scope.Thread)
+ public static class Data {
+ private BinaryFormat binaryFormat;
+ private SpanContext spanContext;
+ private byte[] spanContextBinary;
+
+ @Setup
+ public void setup() {
+ binaryFormat = new BinaryFormatImpl();
+ Random random = new Random(1234);
+ spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ TraceOptions.builder().setIsSampled(random.nextBoolean()).build(),
+ Tracestate.builder().build());
+ spanContextBinary = binaryFormat.toByteArray(spanContext);
+ }
+ }
+
+ /**
+ * This benchmark attempts to measure performance of {@link
+ * BinaryFormat#toBinaryValue(SpanContext)}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public byte[] toBinarySpanContext(Data data) {
+ return data.binaryFormat.toByteArray(data.spanContext);
+ }
+
+ /**
+ * This benchmark attempts to measure performance of {@link BinaryFormat#fromBinaryValue(byte[])}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public SpanContext fromBinarySpanContext(Data data) throws SpanContextParseException {
+ return data.binaryFormat.fromByteArray(data.spanContextBinary);
+ }
+
+ /**
+ * This benchmark attempts to measure performance of {@link
+ * BinaryFormat#toBinaryValue(SpanContext)} then {@link BinaryFormat#fromBinaryValue(byte[])}.
+ */
+ @Benchmark
+ @BenchmarkMode(Mode.SampleTime)
+ @OutputTimeUnit(TimeUnit.NANOSECONDS)
+ public SpanContext toFromBinarySpanContext(Data data) throws SpanContextParseException {
+ return data.binaryFormat.fromByteArray(data.binaryFormat.toByteArray(data.spanContext));
+ }
+}
diff --git a/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/TextFormatBenchmarkBase.java b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/TextFormatBenchmarkBase.java
new file mode 100644
index 00000000..14636920
--- /dev/null
+++ b/impl_core/src/jmh/java/io/opencensus/implcore/trace/propagation/TextFormatBenchmarkBase.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018, 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.trace.propagation;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Generic benchmarks for {@link io.opencensus.trace.propagation.TextFormat}. */
+final class TextFormatBenchmarkBase {
+ private static final Setter<Map<String, String>> setter =
+ new Setter<Map<String, String>>() {
+ @Override
+ public void put(Map<String, String> carrier, String key, String value) {
+ carrier.put(key, value);
+ }
+ };
+
+ private static final Getter<Map<String, String>> getter =
+ new Getter<Map<String, String>>() {
+ @Nullable
+ @Override
+ public String get(Map<String, String> carrier, String key) {
+ return carrier.get(key);
+ }
+ };
+
+ private final TextFormat textFormat;
+
+ TextFormatBenchmarkBase(TextFormat textFormat) {
+ this.textFormat = textFormat;
+ }
+
+ void inject(SpanContext spanContext, Map<String, String> carrier) {
+ textFormat.inject(spanContext, carrier, setter);
+ }
+
+ SpanContext extract(Map<String, String> carrier) throws SpanContextParseException {
+ return textFormat.extract(carrier, getter);
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/common/MillisClock.java b/impl_core/src/main/java/io/opencensus/implcore/common/MillisClock.java
new file mode 100644
index 00000000..98626926
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/common/MillisClock.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.common;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** A {@link Clock} that uses {@link System#currentTimeMillis()} and {@link System#nanoTime()}. */
+@ThreadSafe
+public final class MillisClock extends Clock {
+ private static final MillisClock INSTANCE = new MillisClock();
+
+ private MillisClock() {}
+
+ /**
+ * Returns a {@code MillisClock}.
+ *
+ * @return a {@code MillisClock}.
+ */
+ public static MillisClock getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public Timestamp now() {
+ return Timestamp.fromMillis(System.currentTimeMillis());
+ }
+
+ @Override
+ public long nowNanos() {
+ return System.nanoTime();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/CheckerFrameworkUtils.java b/impl_core/src/main/java/io/opencensus/implcore/internal/CheckerFrameworkUtils.java
new file mode 100644
index 00000000..f08289cf
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/CheckerFrameworkUtils.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.internal;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility methods for suppressing nullness warnings and working around Checker Framework issues.
+ */
+public final class CheckerFrameworkUtils {
+ private CheckerFrameworkUtils() {}
+
+ /** Suppresses warnings about a nullable value. */
+ // TODO(sebright): Try to remove all uses of this method.
+ @SuppressWarnings("nullness")
+ public static <T> T castNonNull(@Nullable T arg) {
+ return arg;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/CurrentState.java b/impl_core/src/main/java/io/opencensus/implcore/internal/CurrentState.java
new file mode 100644
index 00000000..d7b1b112
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/CurrentState.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import java.util.concurrent.atomic.AtomicReference;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** The current state base implementation for stats and tags. */
+@ThreadSafe
+public final class CurrentState {
+
+ /** Current state for stats or tags. */
+ public enum State {
+ /** State that fully enables stats collection or tag propagation. */
+ ENABLED,
+
+ /** State that disables stats collection or tag propagation. */
+ DISABLED
+ }
+
+ private enum InternalState {
+ // Enabled and not read.
+ ENABLED_NOT_READ(State.ENABLED, false),
+
+ // Enabled and read.
+ ENABLED_READ(State.ENABLED, true),
+
+ // Disable and not read.
+ DISABLED_NOT_READ(State.DISABLED, false),
+
+ // Disable and read.
+ DISABLED_READ(State.DISABLED, true);
+
+ private final State state;
+ private final boolean isRead;
+
+ InternalState(State state, boolean isRead) {
+ this.state = state;
+ this.isRead = isRead;
+ }
+ }
+
+ private final AtomicReference<InternalState> currentInternalState;
+
+ /**
+ * Constructs a new {@code CurrentState}.
+ *
+ * @param defaultState the default initial state.
+ */
+ public CurrentState(State defaultState) {
+ this.currentInternalState =
+ new AtomicReference<InternalState>(
+ defaultState == State.ENABLED
+ ? InternalState.ENABLED_NOT_READ
+ : InternalState.DISABLED_NOT_READ);
+ }
+
+ /**
+ * Returns the current state and updates the status as being read.
+ *
+ * @return the current state and updates the status as being read.
+ */
+ public State get() {
+ InternalState internalState = currentInternalState.get();
+ while (!internalState.isRead) {
+ // Slow path, the state is first time read. Change the state only if no other changes
+ // happened between the moment initialState is read and this moment. This ensures that this
+ // method only changes the isRead part of the internal state.
+ currentInternalState.compareAndSet(
+ internalState,
+ internalState.state == State.ENABLED
+ ? InternalState.ENABLED_READ
+ : InternalState.DISABLED_READ);
+ internalState = currentInternalState.get();
+ }
+ return internalState.state;
+ }
+
+ /**
+ * Returns the current state without updating the status as being read.
+ *
+ * @return the current state without updating the status as being read.
+ */
+ public State getInternal() {
+ return currentInternalState.get().state;
+ }
+
+ /**
+ * Sets current state to the given state. Returns true if the current state is changed, false
+ * otherwise.
+ *
+ * @param state the state to be set.
+ * @return true if the current state is changed, false otherwise.
+ */
+ public boolean set(State state) {
+ while (true) {
+ InternalState internalState = currentInternalState.get();
+ checkState(!internalState.isRead, "State was already read, cannot set state.");
+ if (state == internalState.state) {
+ return false;
+ } else {
+ if (!currentInternalState.compareAndSet(
+ internalState,
+ state == State.ENABLED
+ ? InternalState.ENABLED_NOT_READ
+ : InternalState.DISABLED_NOT_READ)) {
+ // The state was changed between the moment the internalState was read and this point.
+ // Some conditions may be not correct, reset at the beginning and recheck all conditions.
+ continue;
+ }
+ return true;
+ }
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/DaemonThreadFactory.java b/impl_core/src/main/java/io/opencensus/implcore/internal/DaemonThreadFactory.java
new file mode 100644
index 00000000..2baa5000
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/DaemonThreadFactory.java
@@ -0,0 +1,53 @@
+/*
+ * 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.internal;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** A {@link ThreadFactory} implementation that starts all {@link Thread} as daemons. */
+public final class DaemonThreadFactory implements ThreadFactory {
+ // AppEngine runtimes have constraints on threading and socket handling
+ // that need to be accommodated.
+ public static final boolean IS_RESTRICTED_APPENGINE =
+ System.getProperty("com.google.appengine.runtime.environment") != null
+ && "1.7".equals(System.getProperty("java.specification.version"));
+ private static final String DELIMITER = "-";
+ private static final ThreadFactory threadFactory = MoreExecutors.platformThreadFactory();
+ private final AtomicInteger threadIdGen = new AtomicInteger();
+ private final String threadPrefix;
+
+ /**
+ * Constructs a new {@code DaemonThreadFactory}.
+ *
+ * @param threadPrefix used to prefix all thread names. (E.g. "CensusDisruptor").
+ */
+ public DaemonThreadFactory(String threadPrefix) {
+ this.threadPrefix = threadPrefix + DELIMITER;
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread thread = threadFactory.newThread(r);
+ if (!IS_RESTRICTED_APPENGINE) {
+ thread.setName(threadPrefix + threadIdGen.getAndIncrement());
+ thread.setDaemon(true);
+ }
+ return thread;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/EventQueue.java b/impl_core/src/main/java/io/opencensus/implcore/internal/EventQueue.java
new file mode 100644
index 00000000..6eb1149a
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/EventQueue.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.internal;
+
+/** A queue that processes events. See {@code DisruptorEventQueue} for an example. */
+public interface EventQueue {
+ void enqueue(Entry entry);
+
+ void shutdown();
+
+ /**
+ * Base interface to be used for all entries in {@link EventQueue}. For example usage, see {@code
+ * DisruptorEventQueue}.
+ */
+ interface Entry {
+ /**
+ * Process the event associated with this entry. This will be called for every event in the
+ * associated {@link EventQueue}.
+ */
+ void process();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/NoopScope.java b/impl_core/src/main/java/io/opencensus/implcore/internal/NoopScope.java
new file mode 100644
index 00000000..51efe894
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/NoopScope.java
@@ -0,0 +1,38 @@
+/*
+ * 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.internal;
+
+import io.opencensus.common.Scope;
+
+/** A {@link Scope} that does nothing when it is created or closed. */
+public final class NoopScope implements Scope {
+ private static final Scope INSTANCE = new NoopScope();
+
+ private NoopScope() {}
+
+ /**
+ * Returns a {@code NoopScope}.
+ *
+ * @return a {@code NoopScope}.
+ */
+ public static Scope getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public void close() {}
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/SimpleEventQueue.java b/impl_core/src/main/java/io/opencensus/implcore/internal/SimpleEventQueue.java
new file mode 100644
index 00000000..58c61c89
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/SimpleEventQueue.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.internal;
+
+/**
+ * An {@link EventQueue} that processes events in the current thread. This class can be used for
+ * testing.
+ */
+public class SimpleEventQueue implements EventQueue {
+
+ @Override
+ public void enqueue(Entry entry) {
+ entry.process();
+ }
+
+ @Override
+ public void shutdown() {}
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/TimestampConverter.java b/impl_core/src/main/java/io/opencensus/implcore/internal/TimestampConverter.java
new file mode 100644
index 00000000..c70f5860
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/TimestampConverter.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.internal;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * This class provides a mechanism for converting {@link System#nanoTime() nanoTime} values to
+ * {@link Timestamp}.
+ */
+@Immutable
+public final class TimestampConverter {
+ private final Timestamp timestamp;
+ private final long nanoTime;
+
+ // Returns a WallTimeConverter initialized to now.
+ public static TimestampConverter now(Clock clock) {
+ return new TimestampConverter(clock.now(), clock.nowNanos());
+ }
+
+ /**
+ * Converts a {@link System#nanoTime() nanoTime} value to {@link Timestamp}.
+ *
+ * @param nanoTime value to convert.
+ * @return the {@code Timestamp} representation of the {@code time}.
+ */
+ public Timestamp convertNanoTime(long nanoTime) {
+ return timestamp.addNanos(nanoTime - this.nanoTime);
+ }
+
+ private TimestampConverter(Timestamp timestamp, long nanoTime) {
+ this.timestamp = timestamp;
+ this.nanoTime = nanoTime;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/Utils.java b/impl_core/src/main/java/io/opencensus/implcore/internal/Utils.java
new file mode 100644
index 00000000..05a039b9
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/Utils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import java.util.List;
+
+/** General internal utility methods. */
+public final class Utils {
+
+ private Utils() {}
+
+ /**
+ * Throws a {@link NullPointerException} if any of the list elements is null.
+ *
+ * @param list the argument list to check for null.
+ * @param errorMessage the message to use for the exception. Will be converted to a string using
+ * {@link String#valueOf(Object)}.
+ */
+ public static <T> void checkListElementNotNull(
+ List<T> list, @javax.annotation.Nullable Object errorMessage) {
+ for (T element : list) {
+ if (element == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java b/impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java
new file mode 100644
index 00000000..944f62fd
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/internal/VarInt.java
@@ -0,0 +1,283 @@
+/*
+ * 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.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+/** Common methods to encode and decode varints and varlongs into ByteBuffers and arrays. */
+// CHECKSTYLE:OFF
+@SuppressWarnings("UngroupedOverloads")
+public class VarInt {
+
+ /** Maximum encoded size of 32-bit positive integers (in bytes) */
+ public static final int MAX_VARINT_SIZE = 5;
+
+ /** maximum encoded size of 64-bit longs, and negative 32-bit ints (in bytes) */
+ public static final int MAX_VARLONG_SIZE = 10;
+
+ private VarInt() {}
+
+ /**
+ * Returns the encoding size in bytes of its input value.
+ *
+ * @param i the integer to be measured
+ * @return the encoding size in bytes of its input value
+ */
+ public static int varIntSize(int i) {
+ int result = 0;
+ do {
+ result++;
+ i >>>= 7;
+ } while (i != 0);
+ return result;
+ }
+
+ /**
+ * Reads a varint from src, places its values into the first element of dst and returns the offset
+ * in to src of the first byte after the varint.
+ *
+ * @param src source buffer to retrieve from
+ * @param offset offset within src
+ * @param dst the resulting int value
+ * @return the updated offset after reading the varint
+ */
+ public static int getVarInt(byte[] src, int offset, int[] dst) {
+ int result = 0;
+ int shift = 0;
+ int b;
+ do {
+ if (shift >= 32) {
+ // Out of range
+ throw new IndexOutOfBoundsException("varint too long");
+ }
+ // Get 7 bits from next byte
+ b = src[offset++];
+ result |= (b & 0x7F) << shift;
+ shift += 7;
+ } while ((b & 0x80) != 0);
+ dst[0] = result;
+ return offset;
+ }
+
+ /**
+ * Encodes an integer in a variable-length encoding, 7 bits per byte, into a destination byte[],
+ * following the protocol buffer convention.
+ *
+ * @param v the int value to write to sink
+ * @param sink the sink buffer to write to
+ * @param offset the offset within sink to begin writing
+ * @return the updated offset after writing the varint
+ */
+ public static int putVarInt(int v, byte[] sink, int offset) {
+ do {
+ // Encode next 7 bits + terminator bit
+ int bits = v & 0x7F;
+ v >>>= 7;
+ byte b = (byte) (bits + ((v != 0) ? 0x80 : 0));
+ sink[offset++] = b;
+ } while (v != 0);
+ return offset;
+ }
+
+ /**
+ * Reads a varint from the current position of the given ByteBuffer and returns the decoded value
+ * as 32 bit integer.
+ *
+ * <p>The position of the buffer is advanced to the first byte after the decoded varint.
+ *
+ * @param src the ByteBuffer to get the var int from
+ * @return The integer value of the decoded varint
+ */
+ public static int getVarInt(ByteBuffer src) {
+ int tmp;
+ if ((tmp = src.get()) >= 0) {
+ return tmp;
+ }
+ int result = tmp & 0x7f;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 7;
+ } else {
+ result |= (tmp & 0x7f) << 7;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 14;
+ } else {
+ result |= (tmp & 0x7f) << 14;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 21;
+ } else {
+ result |= (tmp & 0x7f) << 21;
+ result |= (tmp = src.get()) << 28;
+ while (tmp < 0) {
+ // We get into this loop only in the case of overflow.
+ // By doing this, we can call getVarInt() instead of
+ // getVarLong() when we only need an int.
+ tmp = src.get();
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Encodes an integer in a variable-length encoding, 7 bits per byte, to a ByteBuffer sink.
+ *
+ * @param v the value to encode
+ * @param sink the ByteBuffer to add the encoded value
+ */
+ public static void putVarInt(int v, ByteBuffer sink) {
+ while (true) {
+ int bits = v & 0x7f;
+ v >>>= 7;
+ if (v == 0) {
+ sink.put((byte) bits);
+ return;
+ }
+ sink.put((byte) (bits | 0x80));
+ }
+ }
+
+ /**
+ * Reads a varint from the given InputStream and returns the decoded value as an int.
+ *
+ * @param inputStream the InputStream to read from
+ */
+ public static int getVarInt(InputStream inputStream) throws IOException {
+ int result = 0;
+ int shift = 0;
+ int b;
+ do {
+ if (shift >= 32) {
+ // Out of range
+ throw new IndexOutOfBoundsException("varint too long");
+ }
+ // Get 7 bits from next byte
+ b = inputStream.read();
+ result |= (b & 0x7F) << shift;
+ shift += 7;
+ } while ((b & 0x80) != 0);
+ return result;
+ }
+
+ /**
+ * Encodes an integer in a variable-length encoding, 7 bits per byte, and writes it to the given
+ * OutputStream.
+ *
+ * @param v the value to encode
+ * @param outputStream the OutputStream to write to
+ */
+ public static void putVarInt(int v, OutputStream outputStream) throws IOException {
+ byte[] bytes = new byte[varIntSize(v)];
+ putVarInt(v, bytes, 0);
+ outputStream.write(bytes);
+ }
+
+ /**
+ * Returns the encoding size in bytes of its input value.
+ *
+ * @param v the long to be measured
+ * @return the encoding size in bytes of a given long value.
+ */
+ public static int varLongSize(long v) {
+ int result = 0;
+ do {
+ result++;
+ v >>>= 7;
+ } while (v != 0);
+ return result;
+ }
+
+ /**
+ * Reads an up to 64 bit long varint from the current position of the given ByteBuffer and returns
+ * the decoded value as long.
+ *
+ * <p>The position of the buffer is advanced to the first byte after the decoded varint.
+ *
+ * @param src the ByteBuffer to get the var int from
+ * @return The integer value of the decoded long varint
+ */
+ public static long getVarLong(ByteBuffer src) {
+ long tmp;
+ if ((tmp = src.get()) >= 0) {
+ return tmp;
+ }
+ long result = tmp & 0x7f;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 7;
+ } else {
+ result |= (tmp & 0x7f) << 7;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 14;
+ } else {
+ result |= (tmp & 0x7f) << 14;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 21;
+ } else {
+ result |= (tmp & 0x7f) << 21;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 28;
+ } else {
+ result |= (tmp & 0x7f) << 28;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 35;
+ } else {
+ result |= (tmp & 0x7f) << 35;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 42;
+ } else {
+ result |= (tmp & 0x7f) << 42;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 49;
+ } else {
+ result |= (tmp & 0x7f) << 49;
+ if ((tmp = src.get()) >= 0) {
+ result |= tmp << 56;
+ } else {
+ result |= (tmp & 0x7f) << 56;
+ result |= ((long) src.get()) << 63;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Encodes a long integer in a variable-length encoding, 7 bits per byte, to a ByteBuffer sink.
+ *
+ * @param v the value to encode
+ * @param sink the ByteBuffer to add the encoded value
+ */
+ public static void putVarLong(long v, ByteBuffer sink) {
+ while (true) {
+ int bits = ((int) v) & 0x7f;
+ v >>>= 7;
+ if (v == 0) {
+ sink.put((byte) bits);
+ return;
+ }
+ sink.put((byte) (bits | 0x80));
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImpl.java
new file mode 100644
index 00000000..b7104c9b
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImpl.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.DerivedDoubleGauge;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Implementation of {@link DerivedDoubleGauge}. */
+public final class DerivedDoubleGaugeImpl extends DerivedDoubleGauge implements Meter {
+ private final MetricDescriptor metricDescriptor;
+ private final int labelKeysSize;
+
+ @SuppressWarnings("rawtypes")
+ private volatile Map<List<LabelValue>, PointWithFunction> registeredPoints =
+ Collections.<List<LabelValue>, PointWithFunction>emptyMap();
+
+ DerivedDoubleGaugeImpl(String name, String description, String unit, List<LabelKey> labelKeys) {
+ labelKeysSize = labelKeys.size();
+ this.metricDescriptor =
+ MetricDescriptor.create(name, description, unit, Type.GAUGE_DOUBLE, labelKeys);
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public synchronized <T> void createTimeSeries(
+ List<LabelValue> labelValues,
+ /*@Nullable*/ T obj,
+ ToDoubleFunction</*@Nullable*/ T> function) {
+ Utils.checkListElementNotNull(
+ checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+ checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+ checkNotNull(function, "function");
+
+ List<LabelValue> labelValuesCopy =
+ Collections.<LabelValue>unmodifiableList(new ArrayList<LabelValue>(labelValues));
+
+ PointWithFunction existingPoint = registeredPoints.get(labelValuesCopy);
+ if (existingPoint != null) {
+ throw new IllegalArgumentException(
+ "A different time series with the same labels already exists.");
+ }
+
+ PointWithFunction newPoint = new PointWithFunction<T>(labelValuesCopy, obj, function);
+ // Updating the map of time series happens under a lock to avoid multiple add operations
+ // to happen in the same time.
+ Map<List<LabelValue>, PointWithFunction> registeredPointsCopy =
+ new LinkedHashMap<List<LabelValue>, PointWithFunction>(registeredPoints);
+ registeredPointsCopy.put(labelValuesCopy, newPoint);
+ registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public synchronized void removeTimeSeries(List<LabelValue> labelValues) {
+ checkNotNull(labelValues, "labelValues");
+
+ Map<List<LabelValue>, PointWithFunction> registeredPointsCopy =
+ new LinkedHashMap<List<LabelValue>, PointWithFunction>(registeredPoints);
+ if (registeredPointsCopy.remove(labelValues) == null) {
+ // The element not present, no need to update the current map of time series.
+ return;
+ }
+ registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public synchronized void clear() {
+ registeredPoints = Collections.<List<LabelValue>, PointWithFunction>emptyMap();
+ }
+
+ /*@Nullable*/
+ @Override
+ @SuppressWarnings("rawtypes")
+ public Metric getMetric(Clock clock) {
+ Map<List<LabelValue>, PointWithFunction> currentRegisteredPoints = registeredPoints;
+ if (currentRegisteredPoints.isEmpty()) {
+ return null;
+ }
+
+ if (currentRegisteredPoints.size() == 1) {
+ PointWithFunction point = currentRegisteredPoints.values().iterator().next();
+ return Metric.createWithOneTimeSeries(metricDescriptor, point.getTimeSeries(clock));
+ }
+
+ List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>(currentRegisteredPoints.size());
+ for (Map.Entry<List<LabelValue>, PointWithFunction> entry :
+ currentRegisteredPoints.entrySet()) {
+ timeSeriesList.add(entry.getValue().getTimeSeries(clock));
+ }
+ return Metric.create(metricDescriptor, timeSeriesList);
+ }
+
+ /** Implementation of {@link PointWithFunction} with an object and a callback function. */
+ public static final class PointWithFunction<T> {
+ private final List<LabelValue> labelValues;
+ @javax.annotation.Nullable private final WeakReference<T> ref;
+ private final ToDoubleFunction</*@Nullable*/ T> function;
+
+ PointWithFunction(
+ List<LabelValue> labelValues,
+ /*@Nullable*/ T obj,
+ ToDoubleFunction</*@Nullable*/ T> function) {
+ this.labelValues = labelValues;
+ ref = obj != null ? new WeakReference<T>(obj) : null;
+ this.function = function;
+ }
+
+ private TimeSeries getTimeSeries(Clock clock) {
+ final T obj = ref != null ? ref.get() : null;
+ double value = function.applyAsDouble(obj);
+
+ // TODO(mayurkale): OPTIMIZATION: Avoid re-evaluate the labelValues all the time (issue#1490).
+ return TimeSeries.createWithOnePoint(
+ labelValues, Point.create(Value.doubleValue(value), clock.now()), null);
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedLongGaugeImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedLongGaugeImpl.java
new file mode 100644
index 00000000..90e3e706
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/DerivedLongGaugeImpl.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.DerivedLongGauge;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** Implementation of {@link DerivedLongGauge}. */
+public final class DerivedLongGaugeImpl extends DerivedLongGauge implements Meter {
+ private final MetricDescriptor metricDescriptor;
+ private final int labelKeysSize;
+
+ @SuppressWarnings("rawtypes")
+ private volatile Map<List<LabelValue>, PointWithFunction> registeredPoints =
+ Collections.<List<LabelValue>, PointWithFunction>emptyMap();
+
+ DerivedLongGaugeImpl(String name, String description, String unit, List<LabelKey> labelKeys) {
+ labelKeysSize = labelKeys.size();
+ this.metricDescriptor =
+ MetricDescriptor.create(name, description, unit, Type.GAUGE_INT64, labelKeys);
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public synchronized <T> void createTimeSeries(
+ List<LabelValue> labelValues, /*@Nullable*/ T obj, ToLongFunction</*@Nullable*/ T> function) {
+ Utils.checkListElementNotNull(
+ checkNotNull(labelValues, "labelValues"), "labelValue element should not be null.");
+ checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+ checkNotNull(function, "function");
+
+ List<LabelValue> labelValuesCopy =
+ Collections.unmodifiableList(new ArrayList<LabelValue>(labelValues));
+
+ PointWithFunction existingPoint = registeredPoints.get(labelValuesCopy);
+ if (existingPoint != null) {
+ throw new IllegalArgumentException(
+ "A different time series with the same labels already exists.");
+ }
+
+ PointWithFunction newPoint = new PointWithFunction<T>(labelValuesCopy, obj, function);
+ // Updating the map of time series happens under a lock to avoid multiple add operations
+ // to happen in the same time.
+ Map<List<LabelValue>, PointWithFunction> registeredPointsCopy =
+ new LinkedHashMap<List<LabelValue>, PointWithFunction>(registeredPoints);
+ registeredPointsCopy.put(labelValuesCopy, newPoint);
+ registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public synchronized void removeTimeSeries(List<LabelValue> labelValues) {
+ checkNotNull(labelValues, "labelValues");
+
+ Map<List<LabelValue>, PointWithFunction> registeredPointsCopy =
+ new LinkedHashMap<List<LabelValue>, PointWithFunction>(registeredPoints);
+ if (registeredPointsCopy.remove(labelValues) == null) {
+ // The element not present, no need to update the current map of time series.
+ return;
+ }
+ registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public synchronized void clear() {
+ registeredPoints = Collections.<List<LabelValue>, PointWithFunction>emptyMap();
+ }
+
+ /*@Nullable*/
+ @Override
+ @SuppressWarnings("rawtypes")
+ public Metric getMetric(Clock clock) {
+ Map<List<LabelValue>, PointWithFunction> currentRegisteredPoints = registeredPoints;
+ if (currentRegisteredPoints.isEmpty()) {
+ return null;
+ }
+
+ if (currentRegisteredPoints.size() == 1) {
+ PointWithFunction point = currentRegisteredPoints.values().iterator().next();
+ return Metric.createWithOneTimeSeries(metricDescriptor, point.getTimeSeries(clock));
+ }
+
+ List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>(currentRegisteredPoints.size());
+ for (Map.Entry<List<LabelValue>, PointWithFunction> entry :
+ currentRegisteredPoints.entrySet()) {
+ timeSeriesList.add(entry.getValue().getTimeSeries(clock));
+ }
+ return Metric.create(metricDescriptor, timeSeriesList);
+ }
+
+ /** Implementation of {@link PointWithFunction} with an object and a callback function. */
+ public static final class PointWithFunction<T> {
+ private final List<LabelValue> labelValues;
+ @javax.annotation.Nullable private final WeakReference<T> ref;
+ private final ToLongFunction</*@Nullable*/ T> function;
+
+ PointWithFunction(
+ List<LabelValue> labelValues,
+ /*@Nullable*/ T obj,
+ ToLongFunction</*@Nullable*/ T> function) {
+ this.labelValues = labelValues;
+ ref = obj != null ? new WeakReference<T>(obj) : null;
+ this.function = function;
+ }
+
+ private TimeSeries getTimeSeries(Clock clock) {
+ final T obj = ref != null ? ref.get() : null;
+ long value = function.applyAsLong(obj);
+
+ // TODO(mayurkale): OPTIMIZATION: Avoid re-evaluate the labelValues all the time (issue#1490).
+ return TimeSeries.createWithOnePoint(
+ labelValues, Point.create(Value.longValue(value), clock.now()), null);
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/DoubleGaugeImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/DoubleGaugeImpl.java
new file mode 100644
index 00000000..c314e980
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/DoubleGaugeImpl.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.AtomicDouble;
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.DoubleGauge;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/** Implementation of {@link DoubleGauge}. */
+public final class DoubleGaugeImpl extends DoubleGauge implements Meter {
+ @VisibleForTesting static final LabelValue UNSET_VALUE = LabelValue.create(null);
+
+ private final MetricDescriptor metricDescriptor;
+ private volatile Map<List<LabelValue>, PointImpl> registeredPoints =
+ Collections.<List<LabelValue>, PointImpl>emptyMap();
+ private final int labelKeysSize;
+ private final List<LabelValue> defaultLabelValues;
+
+ DoubleGaugeImpl(String name, String description, String unit, List<LabelKey> labelKeys) {
+ labelKeysSize = labelKeys.size();
+ this.metricDescriptor =
+ MetricDescriptor.create(name, description, unit, Type.GAUGE_DOUBLE, labelKeys);
+
+ // initialize defaultLabelValues
+ defaultLabelValues = new ArrayList<LabelValue>(labelKeysSize);
+ for (int i = 0; i < labelKeysSize; i++) {
+ defaultLabelValues.add(UNSET_VALUE);
+ }
+ }
+
+ @Override
+ public DoublePoint getOrCreateTimeSeries(List<LabelValue> labelValues) {
+ // lock free point retrieval, if it is present
+ PointImpl existingPoint = registeredPoints.get(labelValues);
+ if (existingPoint != null) {
+ return existingPoint;
+ }
+
+ List<LabelValue> labelValuesCopy =
+ Collections.unmodifiableList(
+ new ArrayList<LabelValue>(checkNotNull(labelValues, "labelValues")));
+ return registerTimeSeries(labelValuesCopy);
+ }
+
+ @Override
+ public DoublePoint getDefaultTimeSeries() {
+ // lock free default point retrieval, if it is present
+ PointImpl existingPoint = registeredPoints.get(defaultLabelValues);
+ if (existingPoint != null) {
+ return existingPoint;
+ }
+ return registerTimeSeries(Collections.unmodifiableList(defaultLabelValues));
+ }
+
+ @Override
+ public synchronized void removeTimeSeries(List<LabelValue> labelValues) {
+ checkNotNull(labelValues, "labelValues");
+
+ Map<List<LabelValue>, PointImpl> registeredPointsCopy =
+ new LinkedHashMap<List<LabelValue>, PointImpl>(registeredPoints);
+ if (registeredPointsCopy.remove(labelValues) == null) {
+ // The element not present, no need to update the current map of points.
+ return;
+ }
+ registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+ }
+
+ @Override
+ public synchronized void clear() {
+ registeredPoints = Collections.<List<LabelValue>, PointImpl>emptyMap();
+ }
+
+ private synchronized DoublePoint registerTimeSeries(List<LabelValue> labelValues) {
+ PointImpl existingPoint = registeredPoints.get(labelValues);
+ if (existingPoint != null) {
+ // Return a Point that are already registered. This can happen if a multiple threads
+ // concurrently try to register the same {@code TimeSeries}.
+ return existingPoint;
+ }
+
+ checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+ Utils.checkListElementNotNull(labelValues, "labelValue element should not be null.");
+
+ PointImpl newPoint = new PointImpl(labelValues);
+ // Updating the map of points happens under a lock to avoid multiple add operations
+ // to happen in the same time.
+ Map<List<LabelValue>, PointImpl> registeredPointsCopy =
+ new LinkedHashMap<List<LabelValue>, PointImpl>(registeredPoints);
+ registeredPointsCopy.put(labelValues, newPoint);
+ registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+
+ return newPoint;
+ }
+
+ @Nullable
+ @Override
+ public Metric getMetric(Clock clock) {
+ Map<List<LabelValue>, PointImpl> currentRegisteredPoints = registeredPoints;
+ if (currentRegisteredPoints.isEmpty()) {
+ return null;
+ }
+
+ if (currentRegisteredPoints.size() == 1) {
+ PointImpl point = currentRegisteredPoints.values().iterator().next();
+ return Metric.createWithOneTimeSeries(metricDescriptor, point.getTimeSeries(clock));
+ }
+
+ List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>(currentRegisteredPoints.size());
+ for (Map.Entry<List<LabelValue>, PointImpl> entry : currentRegisteredPoints.entrySet()) {
+ timeSeriesList.add(entry.getValue().getTimeSeries(clock));
+ }
+ return Metric.create(metricDescriptor, timeSeriesList);
+ }
+
+ /** Implementation of {@link DoubleGauge.DoublePoint}. */
+ public static final class PointImpl extends DoublePoint {
+
+ // TODO(mayurkale): Consider to use DoubleAdder here, once we upgrade to Java8.
+ private final AtomicDouble value = new AtomicDouble(0);
+ private final List<LabelValue> labelValues;
+
+ PointImpl(List<LabelValue> labelValues) {
+ this.labelValues = labelValues;
+ }
+
+ @Override
+ public void add(double amt) {
+ value.addAndGet(amt);
+ }
+
+ @Override
+ public void set(double val) {
+ value.set(val);
+ }
+
+ private TimeSeries getTimeSeries(Clock clock) {
+ return TimeSeries.createWithOnePoint(
+ labelValues, Point.create(Value.doubleValue(value.get()), clock.now()), null);
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/LongGaugeImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/LongGaugeImpl.java
new file mode 100644
index 00000000..3460d7a4
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/LongGaugeImpl.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.LongGauge;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import javax.annotation.Nullable;
+
+/** Implementation of {@link LongGauge}. */
+public final class LongGaugeImpl extends LongGauge implements Meter {
+ @VisibleForTesting static final LabelValue UNSET_VALUE = LabelValue.create(null);
+
+ private final MetricDescriptor metricDescriptor;
+ private volatile Map<List<LabelValue>, PointImpl> registeredPoints =
+ Collections.<List<LabelValue>, PointImpl>emptyMap();
+ private final int labelKeysSize;
+ private final List<LabelValue> defaultLabelValues;
+
+ LongGaugeImpl(String name, String description, String unit, List<LabelKey> labelKeys) {
+ labelKeysSize = labelKeys.size();
+ this.metricDescriptor =
+ MetricDescriptor.create(name, description, unit, Type.GAUGE_INT64, labelKeys);
+
+ // initialize defaultLabelValues
+ defaultLabelValues = new ArrayList<LabelValue>(labelKeysSize);
+ for (int i = 0; i < labelKeysSize; i++) {
+ defaultLabelValues.add(UNSET_VALUE);
+ }
+ }
+
+ @Override
+ public LongPoint getOrCreateTimeSeries(List<LabelValue> labelValues) {
+ // lock free point retrieval, if it is present
+ PointImpl existingPoint = registeredPoints.get(labelValues);
+ if (existingPoint != null) {
+ return existingPoint;
+ }
+
+ List<LabelValue> labelValuesCopy =
+ Collections.unmodifiableList(
+ new ArrayList<LabelValue>(checkNotNull(labelValues, "labelValues")));
+ return registerTimeSeries(labelValuesCopy);
+ }
+
+ @Override
+ public LongPoint getDefaultTimeSeries() {
+ // lock free default point retrieval, if it is present
+ PointImpl existingPoint = registeredPoints.get(defaultLabelValues);
+ if (existingPoint != null) {
+ return existingPoint;
+ }
+ return registerTimeSeries(Collections.unmodifiableList(defaultLabelValues));
+ }
+
+ @Override
+ public synchronized void removeTimeSeries(List<LabelValue> labelValues) {
+ checkNotNull(labelValues, "labelValues");
+
+ Map<List<LabelValue>, PointImpl> registeredPointsCopy =
+ new LinkedHashMap<List<LabelValue>, PointImpl>(registeredPoints);
+ if (registeredPointsCopy.remove(labelValues) == null) {
+ // The element not present, no need to update the current map of points.
+ return;
+ }
+ registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+ }
+
+ @Override
+ public synchronized void clear() {
+ registeredPoints = Collections.<List<LabelValue>, PointImpl>emptyMap();
+ }
+
+ private synchronized LongPoint registerTimeSeries(List<LabelValue> labelValues) {
+ PointImpl existingPoint = registeredPoints.get(labelValues);
+ if (existingPoint != null) {
+ // Return a Point that are already registered. This can happen if a multiple threads
+ // concurrently try to register the same {@code TimeSeries}.
+ return existingPoint;
+ }
+
+ checkArgument(labelKeysSize == labelValues.size(), "Incorrect number of labels.");
+ Utils.checkListElementNotNull(labelValues, "labelValue element should not be null.");
+
+ PointImpl newPoint = new PointImpl(labelValues);
+ // Updating the map of points happens under a lock to avoid multiple add operations
+ // to happen in the same time.
+ Map<List<LabelValue>, PointImpl> registeredPointsCopy =
+ new LinkedHashMap<List<LabelValue>, PointImpl>(registeredPoints);
+ registeredPointsCopy.put(labelValues, newPoint);
+ registeredPoints = Collections.unmodifiableMap(registeredPointsCopy);
+
+ return newPoint;
+ }
+
+ @Nullable
+ @Override
+ public Metric getMetric(Clock clock) {
+ Map<List<LabelValue>, PointImpl> currentRegisteredPoints = registeredPoints;
+ if (currentRegisteredPoints.isEmpty()) {
+ return null;
+ }
+
+ if (currentRegisteredPoints.size() == 1) {
+ PointImpl point = currentRegisteredPoints.values().iterator().next();
+ return Metric.createWithOneTimeSeries(metricDescriptor, point.getTimeSeries(clock));
+ }
+
+ List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>(currentRegisteredPoints.size());
+ for (Map.Entry<List<LabelValue>, PointImpl> entry : currentRegisteredPoints.entrySet()) {
+ timeSeriesList.add(entry.getValue().getTimeSeries(clock));
+ }
+ return Metric.create(metricDescriptor, timeSeriesList);
+ }
+
+ /** Implementation of {@link LongGauge.LongPoint}. */
+ public static final class PointImpl extends LongPoint {
+
+ // TODO(mayurkale): Consider to use LongAdder here, once we upgrade to Java8.
+ private final AtomicLong value = new AtomicLong(0);
+ private final List<LabelValue> labelValues;
+
+ PointImpl(List<LabelValue> labelValues) {
+ this.labelValues = labelValues;
+ }
+
+ @Override
+ public void add(long amt) {
+ value.addAndGet(amt);
+ }
+
+ @Override
+ public void set(long val) {
+ value.set(val);
+ }
+
+ private TimeSeries getTimeSeries(Clock clock) {
+ return TimeSeries.createWithOnePoint(
+ labelValues, Point.create(Value.longValue(value.get()), clock.now()), null);
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/Meter.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/Meter.java
new file mode 100644
index 00000000..f5a8dc8f
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/Meter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.Clock;
+import io.opencensus.metrics.export.Metric;
+import javax.annotation.Nullable;
+
+interface Meter {
+ /**
+ * Provides a {@link io.opencensus.metrics.export.Metric} with one or more {@link
+ * io.opencensus.metrics.export.TimeSeries}.
+ *
+ * @param clock the clock used to get the time.
+ * @throws NullPointerException if {@code TimeSeries} is not present in {@code Metric}.
+ * @return a {@code Metric}.
+ */
+ @Nullable
+ Metric getMetric(Clock clock);
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricRegistryImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricRegistryImpl.java
new file mode 100644
index 00000000..1a301ecf
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricRegistryImpl.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.Utils;
+import io.opencensus.metrics.DerivedDoubleGauge;
+import io.opencensus.metrics.DerivedLongGauge;
+import io.opencensus.metrics.DoubleGauge;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LongGauge;
+import io.opencensus.metrics.MetricRegistry;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricProducer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Implementation of {@link MetricRegistry}. */
+public final class MetricRegistryImpl extends MetricRegistry {
+ private final RegisteredMeters registeredMeters;
+ private final MetricProducer metricProducer;
+
+ MetricRegistryImpl(Clock clock) {
+ registeredMeters = new RegisteredMeters();
+ metricProducer = new MetricProducerForRegistry(registeredMeters, clock);
+ }
+
+ @Override
+ public LongGauge addLongGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkListElementNotNull(
+ checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ LongGaugeImpl longGaugeMetric =
+ new LongGaugeImpl(
+ checkNotNull(name, "name"),
+ checkNotNull(description, "description"),
+ checkNotNull(unit, "unit"),
+ Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+ registeredMeters.registerMeter(name, longGaugeMetric);
+ return longGaugeMetric;
+ }
+
+ @Override
+ public DoubleGauge addDoubleGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkListElementNotNull(
+ checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ DoubleGaugeImpl doubleGaugeMetric =
+ new DoubleGaugeImpl(
+ checkNotNull(name, "name"),
+ checkNotNull(description, "description"),
+ checkNotNull(unit, "unit"),
+ Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+ registeredMeters.registerMeter(name, doubleGaugeMetric);
+ return doubleGaugeMetric;
+ }
+
+ @Override
+ public DerivedLongGauge addDerivedLongGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkListElementNotNull(
+ checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ DerivedLongGaugeImpl derivedLongGauge =
+ new DerivedLongGaugeImpl(
+ checkNotNull(name, "name"),
+ checkNotNull(description, "description"),
+ checkNotNull(unit, "unit"),
+ Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+ registeredMeters.registerMeter(name, derivedLongGauge);
+ return derivedLongGauge;
+ }
+
+ @Override
+ public DerivedDoubleGauge addDerivedDoubleGauge(
+ String name, String description, String unit, List<LabelKey> labelKeys) {
+ Utils.checkListElementNotNull(
+ checkNotNull(labelKeys, "labelKeys"), "labelKey element should not be null.");
+ DerivedDoubleGaugeImpl derivedDoubleGauge =
+ new DerivedDoubleGaugeImpl(
+ checkNotNull(name, "name"),
+ checkNotNull(description, "description"),
+ checkNotNull(unit, "unit"),
+ Collections.unmodifiableList(new ArrayList<LabelKey>(labelKeys)));
+ registeredMeters.registerMeter(name, derivedDoubleGauge);
+ return derivedDoubleGauge;
+ }
+
+ private static final class RegisteredMeters {
+ private volatile Map<String, Meter> registeredMeters = Collections.emptyMap();
+
+ private Map<String, Meter> getRegisteredMeters() {
+ return registeredMeters;
+ }
+
+ private synchronized void registerMeter(String meterName, Meter meter) {
+ Meter existingMeter = registeredMeters.get(meterName);
+ if (existingMeter != null) {
+ // TODO(mayurkale): Allow users to register the same Meter multiple times without exception.
+ throw new IllegalArgumentException(
+ "A different metric with the same name already registered.");
+ }
+
+ Map<String, Meter> registeredMetersCopy = new LinkedHashMap<String, Meter>(registeredMeters);
+ registeredMetersCopy.put(meterName, meter);
+ registeredMeters = Collections.unmodifiableMap(registeredMetersCopy);
+ }
+ }
+
+ private static final class MetricProducerForRegistry extends MetricProducer {
+ private final RegisteredMeters registeredMeters;
+ private final Clock clock;
+
+ private MetricProducerForRegistry(RegisteredMeters registeredMeters, Clock clock) {
+ this.registeredMeters = registeredMeters;
+ this.clock = clock;
+ }
+
+ @Override
+ public Collection<Metric> getMetrics() {
+ // Get a snapshot of the current registered meters.
+ Map<String, Meter> meters = registeredMeters.getRegisteredMeters();
+ if (meters.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<Metric> metrics = new ArrayList<Metric>(meters.size());
+ for (Map.Entry<String, Meter> entry : meters.entrySet()) {
+ Metric metric = entry.getValue().getMetric(clock);
+ if (metric != null) {
+ metrics.add(metric);
+ }
+ }
+ return metrics;
+ }
+ }
+
+ MetricProducer getMetricProducer() {
+ return metricProducer;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricsComponentImplBase.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricsComponentImplBase.java
new file mode 100644
index 00000000..1aef6727
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/MetricsComponentImplBase.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.metrics.export.ExportComponentImpl;
+import io.opencensus.metrics.MetricsComponent;
+
+/** Implementation of {@link MetricsComponent}. */
+public class MetricsComponentImplBase extends MetricsComponent {
+
+ private final ExportComponentImpl exportComponent;
+ private final MetricRegistryImpl metricRegistry;
+
+ @Override
+ public ExportComponentImpl getExportComponent() {
+ return exportComponent;
+ }
+
+ @Override
+ public MetricRegistryImpl getMetricRegistry() {
+ return metricRegistry;
+ }
+
+ protected MetricsComponentImplBase(Clock clock) {
+ exportComponent = new ExportComponentImpl();
+ metricRegistry = new MetricRegistryImpl(clock);
+ // Register the MetricRegistry's MetricProducer to the global MetricProducerManager.
+ exportComponent.getMetricProducerManager().add(metricRegistry.getMetricProducer());
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/export/ExportComponentImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/export/ExportComponentImpl.java
new file mode 100644
index 00000000..173c3aec
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/export/ExportComponentImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import io.opencensus.metrics.export.ExportComponent;
+import io.opencensus.metrics.export.MetricProducerManager;
+
+/** Implementation of {@link ExportComponent}. */
+public final class ExportComponentImpl extends ExportComponent {
+
+ private final MetricProducerManager metricProducerManager = new MetricProducerManagerImpl();
+
+ @Override
+ public MetricProducerManager getMetricProducerManager() {
+ return metricProducerManager;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImpl.java
new file mode 100644
index 00000000..6f585a10
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImpl.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import com.google.common.base.Preconditions;
+import io.opencensus.metrics.export.MetricProducer;
+import io.opencensus.metrics.export.MetricProducerManager;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Implementation of {@link MetricProducerManager}. */
+@ThreadSafe
+public final class MetricProducerManagerImpl extends MetricProducerManager {
+
+ private volatile Set<MetricProducer> metricProducers =
+ Collections.unmodifiableSet(new LinkedHashSet<MetricProducer>());
+
+ @Override
+ public synchronized void add(MetricProducer metricProducer) {
+ Preconditions.checkNotNull(metricProducer, "metricProducer");
+ // Updating the set of MetricProducers happens under a lock to avoid multiple add or remove
+ // operations to happen in the same time.
+ Set<MetricProducer> newMetricProducers = new LinkedHashSet<MetricProducer>(metricProducers);
+ if (!newMetricProducers.add(metricProducer)) {
+ // The element already present, no need to update the current set of MetricProducers.
+ return;
+ }
+ metricProducers = Collections.unmodifiableSet(newMetricProducers);
+ }
+
+ @Override
+ public synchronized void remove(MetricProducer metricProducer) {
+ Preconditions.checkNotNull(metricProducer, "metricProducer");
+ // Updating the set of MetricProducers happens under a lock to avoid multiple add or remove
+ // operations to happen in the same time.
+ Set<MetricProducer> newMetricProducers = new LinkedHashSet<MetricProducer>(metricProducers);
+ if (!newMetricProducers.remove(metricProducer)) {
+ // The element not present, no need to update the current set of MetricProducers.
+ return;
+ }
+ metricProducers = Collections.unmodifiableSet(newMetricProducers);
+ }
+
+ @Override
+ public Set<MetricProducer> getAllMetricProducer() {
+ return metricProducers;
+ }
+}
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..172db539
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java
@@ -0,0 +1,95 @@
+/*
+ * 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 com.google.common.collect.Maps;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Measure;
+import io.opencensus.tags.TagValue;
+import java.util.List;
+import java.util.Map;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** 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 Measure measure;
+ private final Map<List</*@Nullable*/ TagValue>, MutableAggregation> tagValueAggregationMap =
+ Maps.newHashMap();
+
+ IntervalBucket(Timestamp start, Duration duration, Aggregation aggregation, Measure measure) {
+ this.start = checkNotNull(start, "Start");
+ this.duration = checkNotNull(duration, "Duration");
+ checkArgument(duration.compareTo(ZERO) > 0, "Duration must be positive");
+ this.aggregation = checkNotNull(aggregation, "Aggregation");
+ this.measure = checkNotNull(measure, "measure");
+ }
+
+ Map<List</*@Nullable*/ TagValue>, MutableAggregation> getTagValueAggregationMap() {
+ return tagValueAggregationMap;
+ }
+
+ Timestamp getStart() {
+ return start;
+ }
+
+ // Puts a new value into the internal MutableAggregations, based on the TagValues.
+ void record(
+ List</*@Nullable*/ TagValue> tagValues,
+ double value,
+ Map<String, String> attachments,
+ Timestamp timestamp) {
+ if (!tagValueAggregationMap.containsKey(tagValues)) {
+ tagValueAggregationMap.put(
+ tagValues, RecordUtils.createMutableAggregation(aggregation, measure));
+ }
+ tagValueAggregationMap.get(tagValues).add(value, attachments, timestamp);
+ }
+
+ /*
+ * 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) elapsedTime.toMillis()) / duration.toMillis();
+ }
+
+ 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..ee51796c
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java
@@ -0,0 +1,66 @@
+/*
+ * 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 MeasureMap putAttachment(String key, String value) {
+ builder.putAttachment(key, 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..d867b342
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java
@@ -0,0 +1,138 @@
+/*
+ * 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.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+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();
+ }
+
+ // Returns the contextual information associated with an example value.
+ Map<String, String> getAttachments() {
+ return attachments;
+ }
+
+ private final ArrayList<Measurement> measurements;
+ private final Map<String, String> attachments;
+
+ private MeasureMapInternal(ArrayList<Measurement> measurements, Map<String, String> attachments) {
+ this.measurements = measurements;
+ this.attachments = Collections.unmodifiableMap(new HashMap<String, String>(attachments));
+ }
+
+ /** 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;
+ }
+
+ Builder putAttachment(String key, String value) {
+ this.attachments.put(key, 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, attachments);
+ }
+
+ private final ArrayList<Measurement> measurements = new ArrayList<Measurement>();
+ private final Map<String, String> attachments = new HashMap<String, String>();
+
+ 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..5da0cad8
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java
@@ -0,0 +1,194 @@
+/*
+ * 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 com.google.common.collect.Sets;
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measurement;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagContext;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import javax.annotation.concurrent.GuardedBy;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** A class that stores a singleton map from {@code MeasureName}s to {@link MutableViewData}s. */
+@SuppressWarnings("deprecation")
+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();
+
+ // Cached set of exported views. It must be set to null whenever a view is registered or
+ // unregistered.
+ @javax.annotation.Nullable private volatile Set<View> exportedViews;
+
+ /** Returns a {@link ViewData} corresponding to the given {@link View.Name}. */
+ @javax.annotation.Nullable
+ synchronized ViewData getView(View.Name viewName, Clock clock, State state) {
+ MutableViewData view = getMutableViewData(viewName);
+ return view == null ? null : view.toViewData(clock.now(), state);
+ }
+
+ Set<View> getExportedViews() {
+ Set<View> views = exportedViews;
+ if (views == null) {
+ synchronized (this) {
+ exportedViews = views = filterExportedViews(registeredViews.values());
+ }
+ }
+ return views;
+ }
+
+ // Returns the subset of the given views that should be exported
+ private static Set<View> filterExportedViews(Collection<View> allViews) {
+ Set<View> views = Sets.newHashSet();
+ for (View view : allViews) {
+ if (view.getWindow() instanceof View.AggregationWindow.Cumulative) {
+ views.add(view);
+ }
+ }
+ return Collections.unmodifiableSet(views);
+ }
+
+ /** Enable stats collection for the given {@link View}. */
+ synchronized void registerView(View view, Clock clock) {
+ exportedViews = null;
+ 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);
+ }
+ Timestamp now = clock.now();
+ mutableMap.put(view.getMeasure().getName(), MutableViewData.create(view, now));
+ }
+
+ @javax.annotation.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);
+ }
+
+ // Records stats with a set of tags.
+ synchronized void record(TagContext tags, MeasureMapInternal stats, Timestamp timestamp) {
+ Iterator<Measurement> iterator = stats.iterator();
+ Map<String, String> attachments = stats.getAttachments();
+ while (iterator.hasNext()) {
+ Measurement measurement = iterator.next();
+ Measure measure = measurement.getMeasure();
+ if (!measure.equals(registeredMeasures.get(measure.getName()))) {
+ // unregistered measures will be ignored.
+ continue;
+ }
+ Collection<MutableViewData> viewDataCollection = mutableMap.get(measure.getName());
+ for (MutableViewData viewData : viewDataCollection) {
+ viewData.record(
+ tags, RecordUtils.getDoubleValueFromMeasurement(measurement), timestamp, attachments);
+ }
+ }
+ }
+
+ synchronized List<Metric> getMetrics(Clock clock, State state) {
+ List<Metric> metrics = new ArrayList<Metric>();
+ Timestamp now = clock.now();
+ for (Entry<String, MutableViewData> entry : mutableMap.entries()) {
+ Metric metric = entry.getValue().toMetric(now, state);
+ if (metric != null) {
+ metrics.add(metric);
+ }
+ }
+ return metrics;
+ }
+
+ // 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);
+ }
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MetricProducerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MetricProducerImpl.java
new file mode 100644
index 00000000..7bf92572
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MetricProducerImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018, 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.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricProducer;
+import java.util.Collection;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Implementation of {@link MetricProducer}. */
+@ThreadSafe
+final class MetricProducerImpl extends MetricProducer {
+
+ private final StatsManager statsManager;
+
+ MetricProducerImpl(StatsManager statsManager) {
+ this.statsManager = statsManager;
+ }
+
+ @Override
+ public Collection<Metric> getMetrics() {
+ return statsManager.getMetrics();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MetricUtils.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MetricUtils.java
new file mode 100644
index 00000000..0dfb1d26
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MetricUtils.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018, 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.annotations.VisibleForTesting;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.List;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+@SuppressWarnings("deprecation")
+// Utils to convert Stats data models to Metric data models.
+final class MetricUtils {
+
+ @javax.annotation.Nullable
+ static MetricDescriptor viewToMetricDescriptor(View view) {
+ if (view.getWindow() instanceof View.AggregationWindow.Interval) {
+ // Only creates Metric for cumulative stats.
+ return null;
+ }
+ List<LabelKey> labelKeys = new ArrayList<LabelKey>();
+ for (TagKey tagKey : view.getColumns()) {
+ // TODO: add description
+ labelKeys.add(LabelKey.create(tagKey.getName(), ""));
+ }
+ Measure measure = view.getMeasure();
+ return MetricDescriptor.create(
+ view.getName().asString(),
+ view.getDescription(),
+ measure.getUnit(),
+ getType(measure, view.getAggregation()),
+ labelKeys);
+ }
+
+ @VisibleForTesting
+ static Type getType(Measure measure, Aggregation aggregation) {
+ return aggregation.match(
+ Functions.returnConstant(
+ measure.match(
+ TYPE_CUMULATIVE_DOUBLE_FUNCTION, // Sum Double
+ TYPE_CUMULATIVE_INT64_FUNCTION, // Sum Int64
+ TYPE_UNRECOGNIZED_FUNCTION)),
+ TYPE_CUMULATIVE_INT64_FUNCTION, // Count
+ TYPE_CUMULATIVE_DISTRIBUTION_FUNCTION, // Distribution
+ Functions.returnConstant(
+ measure.match(
+ TYPE_GAUGE_DOUBLE_FUNCTION, // LastValue Double
+ TYPE_GAUGE_INT64_FUNCTION, // LastValue Long
+ TYPE_UNRECOGNIZED_FUNCTION)),
+ AGGREGATION_TYPE_DEFAULT_FUNCTION);
+ }
+
+ static List<LabelValue> tagValuesToLabelValues(List</*@Nullable*/ TagValue> tagValues) {
+ List<LabelValue> labelValues = new ArrayList<LabelValue>();
+ for (/*@Nullable*/ TagValue tagValue : tagValues) {
+ labelValues.add(LabelValue.create(tagValue == null ? null : tagValue.asString()));
+ }
+ return labelValues;
+ }
+
+ private static final Function<Object, Type> TYPE_CUMULATIVE_DOUBLE_FUNCTION =
+ Functions.returnConstant(Type.CUMULATIVE_DOUBLE);
+
+ private static final Function<Object, Type> TYPE_CUMULATIVE_INT64_FUNCTION =
+ Functions.returnConstant(Type.CUMULATIVE_INT64);
+
+ private static final Function<Object, Type> TYPE_CUMULATIVE_DISTRIBUTION_FUNCTION =
+ Functions.returnConstant(Type.CUMULATIVE_DISTRIBUTION);
+
+ private static final Function<Object, Type> TYPE_GAUGE_DOUBLE_FUNCTION =
+ Functions.returnConstant(Type.GAUGE_DOUBLE);
+
+ private static final Function<Object, Type> TYPE_GAUGE_INT64_FUNCTION =
+ Functions.returnConstant(Type.GAUGE_INT64);
+
+ private static final Function<Object, Type> TYPE_UNRECOGNIZED_FUNCTION =
+ Functions.<Type>throwAssertionError();
+
+ private static final Function<Aggregation, Type> AGGREGATION_TYPE_DEFAULT_FUNCTION =
+ new Function<Aggregation, Type>() {
+ @Override
+ public Type apply(Aggregation arg) {
+ if (arg instanceof Aggregation.Mean) {
+ return Type.CUMULATIVE_DOUBLE; // Mean
+ }
+ throw new AssertionError();
+ }
+ };
+
+ private MetricUtils() {}
+}
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..6e2bff1c
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java
@@ -0,0 +1,556 @@
+/*
+ * 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 com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.export.Distribution;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.BucketBoundaries;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** 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
+ * @param attachments the contextual information on an {@link Exemplar}
+ * @param timestamp the timestamp when the value is recorded
+ */
+ abstract void add(double value, Map<String, String> attachments, Timestamp timestamp);
+
+ // TODO(songya): remove this method once interval stats is completely removed.
+ /**
+ * 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);
+
+ abstract AggregationData toAggregationData();
+
+ abstract Point toPoint(Timestamp timestamp);
+
+ /** Calculate sum of doubles on aggregated {@code MeasureValue}s. */
+ static class MutableSumDouble extends MutableAggregation {
+
+ private double sum = 0.0;
+
+ private MutableSumDouble() {}
+
+ /**
+ * Construct a {@code MutableSumDouble}.
+ *
+ * @return an empty {@code MutableSumDouble}.
+ */
+ static MutableSumDouble create() {
+ return new MutableSumDouble();
+ }
+
+ @Override
+ void add(double value, Map<String, String> attachments, Timestamp timestamp) {
+ sum += value;
+ }
+
+ @Override
+ void combine(MutableAggregation other, double fraction) {
+ checkArgument(other instanceof MutableSumDouble, "MutableSumDouble expected.");
+ this.sum += fraction * ((MutableSumDouble) other).sum;
+ }
+
+ @Override
+ AggregationData toAggregationData() {
+ return AggregationData.SumDataDouble.create(sum);
+ }
+
+ @Override
+ Point toPoint(Timestamp timestamp) {
+ return Point.create(Value.doubleValue(sum), timestamp);
+ }
+
+ @VisibleForTesting
+ double getSum() {
+ return sum;
+ }
+ }
+
+ /** Calculate sum of longs on aggregated {@code MeasureValue}s. */
+ static final class MutableSumLong extends MutableSumDouble {
+ private MutableSumLong() {
+ super();
+ }
+
+ /**
+ * Construct a {@code MutableSumLong}.
+ *
+ * @return an empty {@code MutableSumLong}.
+ */
+ static MutableSumLong create() {
+ return new MutableSumLong();
+ }
+
+ @Override
+ AggregationData toAggregationData() {
+ return AggregationData.SumDataLong.create(Math.round(getSum()));
+ }
+
+ @Override
+ Point toPoint(Timestamp timestamp) {
+ return Point.create(Value.longValue(Math.round(getSum())), timestamp);
+ }
+ }
+
+ /** 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, Map<String, String> attachments, Timestamp timestamp) {
+ count++;
+ }
+
+ @Override
+ void combine(MutableAggregation other, double fraction) {
+ checkArgument(other instanceof MutableCount, "MutableCount expected.");
+ this.count += Math.round(fraction * ((MutableCount) other).getCount());
+ }
+
+ @Override
+ AggregationData toAggregationData() {
+ return AggregationData.CountData.create(count);
+ }
+
+ @Override
+ Point toPoint(Timestamp timestamp) {
+ return Point.create(Value.longValue(count), timestamp);
+ }
+
+ /**
+ * Returns the aggregated count.
+ *
+ * @return the aggregated count.
+ */
+ long getCount() {
+ return count;
+ }
+ }
+
+ /** 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, Map<String, String> attachments, Timestamp timestamp) {
+ 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;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ AggregationData toAggregationData() {
+ return AggregationData.MeanData.create(getMean(), count);
+ }
+
+ @Override
+ Point toPoint(Timestamp timestamp) {
+ return Point.create(Value.doubleValue(getMean()), timestamp);
+ }
+
+ /**
+ * Returns the aggregated mean.
+ *
+ * @return the aggregated mean.
+ */
+ double getMean() {
+ return count == 0 ? 0 : sum / count;
+ }
+
+ /**
+ * Returns the aggregated count.
+ *
+ * @return the aggregated count.
+ */
+ long getCount() {
+ return count;
+ }
+
+ @VisibleForTesting
+ double getSum() {
+ return sum;
+ }
+ }
+
+ /** 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;
+
+ // If there's a histogram (i.e bucket boundaries are not empty) in this MutableDistribution,
+ // exemplars will have the same size to bucketCounts; otherwise exemplars are null.
+ // Only the newest exemplar will be kept at each index.
+ @javax.annotation.Nullable private final Exemplar[] exemplars;
+
+ private MutableDistribution(BucketBoundaries bucketBoundaries) {
+ this.bucketBoundaries = bucketBoundaries;
+ int buckets = bucketBoundaries.getBoundaries().size() + 1;
+ this.bucketCounts = new long[buckets];
+ // In the implementation, each histogram bucket can have up to one exemplar, and the exemplar
+ // array is guaranteed to be in ascending order.
+ // If there's no histogram, don't record exemplars.
+ this.exemplars = bucketBoundaries.getBoundaries().isEmpty() ? null : new Exemplar[buckets];
+ }
+
+ /**
+ * 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, Map<String, String> attachments, Timestamp timestamp) {
+ 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;
+ }
+
+ int bucket = 0;
+ for (; bucket < bucketBoundaries.getBoundaries().size(); bucket++) {
+ if (value < bucketBoundaries.getBoundaries().get(bucket)) {
+ break;
+ }
+ }
+ bucketCounts[bucket]++;
+
+ // No implicit recording for exemplars - if there are no attachments (contextual information),
+ // don't record exemplars.
+ if (!attachments.isEmpty() && exemplars != null) {
+ exemplars[bucket] = Exemplar.create(value, timestamp, attachments);
+ }
+ }
+
+ // 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];
+ }
+
+ Exemplar[] otherExemplars = mutableDistribution.getExemplars();
+ if (exemplars != null && otherExemplars != null) {
+ for (int i = 0; i < otherExemplars.length; i++) {
+ Exemplar exemplar = otherExemplars[i];
+ // Assume other is always newer than this, because we combined interval buckets in time
+ // order.
+ // If there's a newer exemplar, overwrite current value.
+ if (exemplar != null) {
+ this.exemplars[i] = exemplar;
+ }
+ }
+ }
+ }
+
+ @Override
+ AggregationData toAggregationData() {
+ List<Long> boxedBucketCounts = new ArrayList<Long>();
+ for (long bucketCount : bucketCounts) {
+ boxedBucketCounts.add(bucketCount);
+ }
+ List<Exemplar> exemplarList = new ArrayList<Exemplar>();
+ if (exemplars != null) {
+ for (Exemplar exemplar : exemplars) {
+ if (exemplar != null) {
+ exemplarList.add(exemplar);
+ }
+ }
+ }
+ return DistributionData.create(
+ mean, count, min, max, sumOfSquaredDeviations, boxedBucketCounts, exemplarList);
+ }
+
+ @Override
+ Point toPoint(Timestamp timestamp) {
+ List<Distribution.Bucket> buckets = new ArrayList<Distribution.Bucket>();
+ for (int bucket = 0; bucket < bucketCounts.length; bucket++) {
+ long bucketCount = bucketCounts[bucket];
+ @javax.annotation.Nullable AggregationData.DistributionData.Exemplar exemplar = null;
+ if (exemplars != null) {
+ exemplar = exemplars[bucket];
+ }
+
+ Distribution.Bucket metricBucket;
+ if (exemplar != null) {
+ // Bucket with an Exemplar.
+ metricBucket =
+ Distribution.Bucket.create(
+ bucketCount,
+ Distribution.Exemplar.create(
+ exemplar.getValue(), exemplar.getTimestamp(), exemplar.getAttachments()));
+ } else {
+ // Bucket with no Exemplar.
+ metricBucket = Distribution.Bucket.create(bucketCount);
+ }
+ buckets.add(metricBucket);
+ }
+
+ // TODO(mayurkale): Drop the first bucket when converting to metrics.
+ // Reason: In Stats API, bucket bounds begin with -infinity (first bucket is (-infinity, 0)).
+ BucketOptions bucketOptions = BucketOptions.explicitOptions(bucketBoundaries.getBoundaries());
+
+ return Point.create(
+ Value.distributionValue(
+ Distribution.create(
+ count, mean * count, sumOfSquaredDeviations, bucketOptions, buckets)),
+ timestamp);
+ }
+
+ 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;
+ }
+
+ BucketBoundaries getBucketBoundaries() {
+ return bucketBoundaries;
+ }
+
+ @javax.annotation.Nullable
+ Exemplar[] getExemplars() {
+ return exemplars;
+ }
+ }
+
+ /** Calculate double last value on aggregated {@code MeasureValue}s. */
+ static class MutableLastValueDouble extends MutableAggregation {
+
+ // Initial value that will get reset as soon as first value is added.
+ private double lastValue = Double.NaN;
+ // TODO(songya): remove this once interval stats is completely removed.
+ private boolean initialized = false;
+
+ private MutableLastValueDouble() {}
+
+ /**
+ * Construct a {@code MutableLastValueDouble}.
+ *
+ * @return an empty {@code MutableLastValueDouble}.
+ */
+ static MutableLastValueDouble create() {
+ return new MutableLastValueDouble();
+ }
+
+ @Override
+ void add(double value, Map<String, String> attachments, Timestamp timestamp) {
+ lastValue = value;
+ // TODO(songya): remove this once interval stats is completely removed.
+ if (!initialized) {
+ initialized = true;
+ }
+ }
+
+ @Override
+ void combine(MutableAggregation other, double fraction) {
+ checkArgument(other instanceof MutableLastValueDouble, "MutableLastValueDouble expected.");
+ MutableLastValueDouble otherValue = (MutableLastValueDouble) other;
+ // Assume other is always newer than this, because we combined interval buckets in time order.
+ // If there's a newer value, overwrite current value.
+ this.lastValue = otherValue.initialized ? otherValue.getLastValue() : this.lastValue;
+ }
+
+ @Override
+ AggregationData toAggregationData() {
+ return AggregationData.LastValueDataDouble.create(lastValue);
+ }
+
+ @Override
+ Point toPoint(Timestamp timestamp) {
+ return Point.create(Value.doubleValue(lastValue), timestamp);
+ }
+
+ @VisibleForTesting
+ double getLastValue() {
+ return lastValue;
+ }
+ }
+
+ /** Calculate last long value on aggregated {@code MeasureValue}s. */
+ static final class MutableLastValueLong extends MutableLastValueDouble {
+ private MutableLastValueLong() {
+ super();
+ }
+
+ /**
+ * Construct a {@code MutableLastValueLong}.
+ *
+ * @return an empty {@code MutableLastValueLong}.
+ */
+ static MutableLastValueLong create() {
+ return new MutableLastValueLong();
+ }
+
+ @Override
+ AggregationData toAggregationData() {
+ return AggregationData.LastValueDataLong.create(Math.round(getLastValue()));
+ }
+
+ @Override
+ Point toPoint(Timestamp timestamp) {
+ return Point.create(Value.longValue(Math.round(getLastValue())), timestamp);
+ }
+ }
+}
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..928675e9
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java
@@ -0,0 +1,464 @@
+/*
+ * 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 io.opencensus.implcore.stats.RecordUtils.createAggregationMap;
+import static io.opencensus.implcore.stats.RecordUtils.createMutableAggregation;
+import static io.opencensus.implcore.stats.RecordUtils.getTagMap;
+import static io.opencensus.implcore.stats.RecordUtils.getTagValues;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.LinkedHashMultimap;
+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.internal.CheckerFrameworkUtils;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+/** A mutable version of {@link ViewData}, used for recording stats and start/end time. */
+@SuppressWarnings("deprecation")
+abstract class MutableViewData {
+
+ @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;
+ }
+
+ @javax.annotation.Nullable
+ abstract Metric toMetric(Timestamp now, State state);
+
+ /** Record stats with the given tags. */
+ abstract void record(
+ TagContext context, double value, Timestamp timestamp, Map<String, String> attachments);
+
+ /** Convert this {@link MutableViewData} to {@link ViewData}. */
+ abstract ViewData toViewData(Timestamp now, State 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 final class CumulativeMutableViewData extends MutableViewData {
+
+ private Timestamp start;
+ private final Map<List</*@Nullable*/ TagValue>, MutableAggregation> tagValueAggregationMap =
+ Maps.newHashMap();
+ // Cache a MetricDescriptor to avoid converting View to MetricDescriptor in the future.
+ private final MetricDescriptor metricDescriptor;
+
+ private CumulativeMutableViewData(View view, Timestamp start) {
+ super(view);
+ this.start = start;
+ MetricDescriptor metricDescriptor = MetricUtils.viewToMetricDescriptor(view);
+ if (metricDescriptor == null) {
+ throw new AssertionError(
+ "Cumulative view should be converted to a non-null MetricDescriptor.");
+ } else {
+ this.metricDescriptor = metricDescriptor;
+ }
+ }
+
+ @javax.annotation.Nullable
+ @Override
+ Metric toMetric(Timestamp now, State state) {
+ if (state == State.DISABLED) {
+ return null;
+ }
+ Type type = metricDescriptor.getType();
+ @javax.annotation.Nullable
+ Timestamp startTime = type == Type.GAUGE_INT64 || type == Type.GAUGE_DOUBLE ? null : start;
+ List<TimeSeries> timeSeriesList = new ArrayList<TimeSeries>();
+ for (Entry<List</*@Nullable*/ TagValue>, MutableAggregation> entry :
+ tagValueAggregationMap.entrySet()) {
+ List<LabelValue> labelValues = MetricUtils.tagValuesToLabelValues(entry.getKey());
+ Point point = entry.getValue().toPoint(now);
+ timeSeriesList.add(TimeSeries.createWithOnePoint(labelValues, point, startTime));
+ }
+ return Metric.create(metricDescriptor, timeSeriesList);
+ }
+
+ @Override
+ void record(
+ TagContext context, double value, Timestamp timestamp, Map<String, String> attachments) {
+ List</*@Nullable*/ TagValue> tagValues =
+ getTagValues(getTagMap(context), super.view.getColumns());
+ if (!tagValueAggregationMap.containsKey(tagValues)) {
+ tagValueAggregationMap.put(
+ tagValues,
+ createMutableAggregation(super.view.getAggregation(), super.getView().getMeasure()));
+ }
+ tagValueAggregationMap.get(tagValues).add(value, attachments, timestamp);
+ }
+
+ @Override
+ ViewData toViewData(Timestamp now, State state) {
+ if (state == State.ENABLED) {
+ return ViewData.create(
+ super.view,
+ createAggregationMap(tagValueAggregationMap, super.view.getMeasure()),
+ ViewData.AggregationWindowData.CumulativeData.create(start, now));
+ } else {
+ // If Stats state is DISABLED, return an empty ViewData.
+ return ViewData.create(
+ super.view,
+ Collections.<List</*@Nullable*/ TagValue>, AggregationData>emptyMap(),
+ ViewData.AggregationWindowData.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 ArrayDeque<IntervalBucket> buckets = new ArrayDeque<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 = ((View.AggregationWindow.Interval) view.getWindow()).getDuration();
+ this.totalDuration = totalDuration;
+ this.bucketDuration = Duration.fromMillis(totalDuration.toMillis() / 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);
+ }
+
+ @javax.annotation.Nullable
+ @Override
+ Metric toMetric(Timestamp now, State state) {
+ return null;
+ }
+
+ @Override
+ void record(
+ TagContext context, double value, Timestamp timestamp, Map<String, String> attachments) {
+ List</*@Nullable*/ TagValue> tagValues =
+ getTagValues(getTagMap(context), super.view.getColumns());
+ refreshBucketList(timestamp);
+ // It is always the last bucket that does the recording.
+ CheckerFrameworkUtils.castNonNull(buckets.peekLast())
+ .record(tagValues, value, attachments, timestamp);
+ }
+
+ @Override
+ ViewData toViewData(Timestamp now, State state) {
+ refreshBucketList(now);
+ if (state == State.ENABLED) {
+ return ViewData.create(
+ super.view,
+ combineBucketsAndGetAggregationMap(now),
+ ViewData.AggregationWindowData.IntervalData.create(now));
+ } else {
+ // If Stats state is DISABLED, return an empty ViewData.
+ return ViewData.create(
+ super.view,
+ Collections.<List</*@Nullable*/ TagValue>, AggregationData>emptyMap(),
+ ViewData.AggregationWindowData.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 =
+ CheckerFrameworkUtils.castNonNull(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 = now.subtractTimestamp(startOfLastBucket).toMillis();
+ long numOfPadBuckets = elapsedTimeMillis / bucketDuration.toMillis();
+
+ 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 =
+ CheckerFrameworkUtils.castNonNull(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(),
+ super.view.getMeasure()));
+ 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</*@Nullable*/ TagValue>, AggregationData> combineBucketsAndGetAggregationMap(
+ Timestamp now) {
+ // Need to maintain the order of inserted MutableAggregations (inserted based on time order).
+ Multimap<List</*@Nullable*/ TagValue>, MutableAggregation> multimap =
+ LinkedHashMultimap.create();
+
+ ArrayDeque<IntervalBucket> shallowCopy = new ArrayDeque<IntervalBucket>(buckets);
+
+ Aggregation aggregation = super.view.getAggregation();
+ Measure measure = super.view.getMeasure();
+ putBucketsIntoMultiMap(shallowCopy, multimap, aggregation, measure, now);
+ Map<List</*@Nullable*/ TagValue>, MutableAggregation> singleMap =
+ aggregateOnEachTagValueList(multimap, aggregation, measure);
+ 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(
+ ArrayDeque<IntervalBucket> buckets,
+ Multimap<List</*@Nullable*/ TagValue>, MutableAggregation> multimap,
+ Aggregation aggregation,
+ Measure measure,
+ Timestamp now) {
+ // Put fractional stats of the head (oldest) bucket.
+ IntervalBucket head = CheckerFrameworkUtils.castNonNull(buckets.peekFirst());
+ IntervalBucket tail = CheckerFrameworkUtils.castNonNull(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, measure, 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</*@Nullable*/ 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,
+ Measure measure,
+ double fraction) {
+ for (Entry<T, MutableAggregation> entry : mutableAggrMap.entrySet()) {
+ // Initially empty MutableAggregations.
+ MutableAggregation fractionalMutableAgg = createMutableAggregation(aggregation, measure);
+ 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, Measure measure) {
+ Map<T, MutableAggregation> map = Maps.newHashMap();
+ for (T tagValues : multimap.keySet()) {
+ // Initially empty MutableAggregations.
+ MutableAggregation combinedAggregation = createMutableAggregation(aggregation, measure);
+ 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()));
+ }
+ }
+
+ private static final class CreateCumulative
+ implements Function<View.AggregationWindow.Cumulative, MutableViewData> {
+ @Override
+ public MutableViewData apply(View.AggregationWindow.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<View.AggregationWindow.Interval, MutableViewData> {
+ @Override
+ public MutableViewData apply(View.AggregationWindow.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/RecordUtils.java b/impl_core/src/main/java/io/opencensus/implcore/stats/RecordUtils.java
new file mode 100644
index 00000000..fbb593f5
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/RecordUtils.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2018, 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.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.implcore.stats.MutableAggregation.MutableCount;
+import io.opencensus.implcore.stats.MutableAggregation.MutableDistribution;
+import io.opencensus.implcore.stats.MutableAggregation.MutableLastValueDouble;
+import io.opencensus.implcore.stats.MutableAggregation.MutableLastValueLong;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSumDouble;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSumLong;
+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.LastValue;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+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 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.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.Nullable;
+*/
+
+@SuppressWarnings("deprecation")
+/* Common static utilities for stats recording. */
+final class RecordUtils {
+
+ @javax.annotation.Nullable @VisibleForTesting static final TagValue UNKNOWN_TAG_VALUE = null;
+
+ 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</*@Nullable*/ TagValue> getTagValues(
+ Map<? extends TagKey, ? extends TagValue> tags, List<? extends TagKey> columns) {
+ List</*@Nullable*/ TagValue> tagValues = new ArrayList</*@Nullable*/ 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 null.
+ tagValues.add(UNKNOWN_TAG_VALUE);
+ } else {
+ tagValues.add(tags.get(tagKey));
+ }
+ }
+ return tagValues;
+ }
+
+ /**
+ * 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, final Measure measure) {
+ return aggregation.match(
+ new Function<Sum, MutableAggregation>() {
+ @Override
+ public MutableAggregation apply(Sum arg) {
+ return measure.match(
+ CreateMutableSumDouble.INSTANCE,
+ CreateMutableSumLong.INSTANCE,
+ Functions.<MutableAggregation>throwAssertionError());
+ }
+ },
+ CreateMutableCount.INSTANCE,
+ CreateMutableDistribution.INSTANCE,
+ new Function<LastValue, MutableAggregation>() {
+ @Override
+ public MutableAggregation apply(LastValue arg) {
+ return measure.match(
+ CreateMutableLastValueDouble.INSTANCE,
+ CreateMutableLastValueLong.INSTANCE,
+ Functions.<MutableAggregation>throwAssertionError());
+ }
+ },
+ AggregationDefaultFunction.INSTANCE);
+ }
+
+ // Covert a mapping from TagValues to MutableAggregation, to a mapping from TagValues to
+ // AggregationData.
+ 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(), entry.getValue().toAggregationData());
+ }
+ return map;
+ }
+
+ static double getDoubleValueFromMeasurement(Measurement measurement) {
+ return measurement.match(
+ GET_VALUE_FROM_MEASUREMENT_DOUBLE,
+ GET_VALUE_FROM_MEASUREMENT_LONG,
+ Functions.<Double>throwAssertionError());
+ }
+
+ // static inner Function classes
+
+ private static final Function<MeasurementDouble, Double> GET_VALUE_FROM_MEASUREMENT_DOUBLE =
+ new Function<MeasurementDouble, Double>() {
+ @Override
+ public Double apply(MeasurementDouble arg) {
+ return arg.getValue();
+ }
+ };
+
+ private static final Function<MeasurementLong, Double> GET_VALUE_FROM_MEASUREMENT_LONG =
+ new Function<MeasurementLong, Double>() {
+ @Override
+ public Double apply(MeasurementLong arg) {
+ // TODO: consider checking truncation here.
+ return (double) arg.getValue();
+ }
+ };
+
+ private static final class CreateMutableSumDouble
+ implements Function<MeasureDouble, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(MeasureDouble arg) {
+ return MutableSumDouble.create();
+ }
+
+ private static final CreateMutableSumDouble INSTANCE = new CreateMutableSumDouble();
+ }
+
+ private static final class CreateMutableSumLong
+ implements Function<MeasureLong, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(MeasureLong arg) {
+ return MutableSumLong.create();
+ }
+
+ private static final CreateMutableSumLong INSTANCE = new CreateMutableSumLong();
+ }
+
+ 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();
+ }
+
+ // TODO(songya): remove this once Mean aggregation is completely removed. Before that
+ // we need to continue supporting Mean, since it could still be used by users and some
+ // deprecated RPC views.
+ private static final class AggregationDefaultFunction
+ implements Function<Aggregation, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(Aggregation arg) {
+ if (arg instanceof Aggregation.Mean) {
+ return MutableMean.create();
+ }
+ throw new IllegalArgumentException("Unknown Aggregation.");
+ }
+
+ private static final AggregationDefaultFunction INSTANCE = new AggregationDefaultFunction();
+ }
+
+ 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 CreateMutableLastValueDouble
+ implements Function<MeasureDouble, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(MeasureDouble arg) {
+ return MutableLastValueDouble.create();
+ }
+
+ private static final CreateMutableLastValueDouble INSTANCE = new CreateMutableLastValueDouble();
+ }
+
+ private static final class CreateMutableLastValueLong
+ implements Function<MeasureLong, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(MeasureLong arg) {
+ return MutableLastValueLong.create();
+ }
+
+ private static final CreateMutableLastValueLong INSTANCE = new CreateMutableLastValueLong();
+ }
+
+ private RecordUtils() {}
+}
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..741b399b
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java
@@ -0,0 +1,92 @@
+/*
+ * 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.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.metrics.Metrics;
+import io.opencensus.metrics.export.MetricProducer;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.StatsComponent;
+
+/** Base implementation of {@link StatsComponent}. */
+public class StatsComponentImplBase extends StatsComponent {
+ private static final State DEFAULT_STATE = State.ENABLED;
+
+ // The State shared between the StatsComponent, StatsRecorder and ViewManager.
+ private final CurrentState currentState = new CurrentState(DEFAULT_STATE);
+
+ 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, currentState);
+ this.viewManager = new ViewManagerImpl(statsManager);
+ this.statsRecorder = new StatsRecorderImpl(statsManager);
+
+ // Create a new MetricProducerImpl and register it to MetricProducerManager when
+ // StatsComponentImplBase is initialized.
+ MetricProducer metricProducer = new MetricProducerImpl(statsManager);
+ Metrics.getExportComponent().getMetricProducerManager().add(metricProducer);
+ }
+
+ @Override
+ public ViewManagerImpl getViewManager() {
+ return viewManager;
+ }
+
+ @Override
+ public StatsRecorderImpl getStatsRecorder() {
+ return statsRecorder;
+ }
+
+ @Override
+ public StatsCollectionState getState() {
+ return stateToStatsState(currentState.get());
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public synchronized void setState(StatsCollectionState newState) {
+ boolean stateChanged =
+ currentState.set(statsStateToState(Preconditions.checkNotNull(newState, "newState")));
+ if (stateChanged) {
+ if (newState == StatsCollectionState.DISABLED) {
+ viewManager.clearStats();
+ } else {
+ viewManager.resumeStatsCollection();
+ }
+ }
+ }
+
+ private static State statsStateToState(StatsCollectionState statsCollectionState) {
+ return statsCollectionState == StatsCollectionState.ENABLED ? State.ENABLED : State.DISABLED;
+ }
+
+ private static StatsCollectionState stateToStatsState(State state) {
+ return state == State.ENABLED ? StatsCollectionState.ENABLED : StatsCollectionState.DISABLED;
+ }
+}
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..17e99d46
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java
@@ -0,0 +1,104 @@
+/*
+ * 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.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagContext;
+import java.util.Collection;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** 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 CurrentState state;
+ private final MeasureToViewMap measureToViewMap = new MeasureToViewMap();
+
+ StatsManager(EventQueue queue, Clock clock, CurrentState 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);
+ }
+
+ @Nullable
+ ViewData getView(View.Name viewName) {
+ return measureToViewMap.getView(viewName, clock, state.getInternal());
+ }
+
+ Set<View> getExportedViews() {
+ return measureToViewMap.getExportedViews();
+ }
+
+ 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() == State.ENABLED) {
+ queue.enqueue(new StatsEvent(this, tags, measurementValues));
+ }
+ }
+
+ Collection<Metric> getMetrics() {
+ return measureToViewMap.getMetrics(clock, state.getInternal());
+ }
+
+ 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..20ea97f8
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/ViewManagerImpl.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.stats;
+
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** 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
+ @Nullable
+ public ViewData getView(View.Name viewName) {
+ return statsManager.getView(viewName);
+ }
+
+ @Override
+ public Set<View> getAllExportedViews() {
+ return statsManager.getExportedViews();
+ }
+
+ 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..e6bb12f5
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java
@@ -0,0 +1,71 @@
+/*
+ * 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;
+
+/**
+ * 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.
+ */
+ 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/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..a17198d8
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextBuilderImpl.java
@@ -0,0 +1,60 @@
+/*
+ * 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) {
+ tags.put(checkNotNull(key, "key"), checkNotNull(value, "value"));
+ return this;
+ }
+
+ @Override
+ public TagContextBuilderImpl remove(TagKey key) {
+ tags.remove(checkNotNull(key, "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..f7a8ff82
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java
@@ -0,0 +1,85 @@
+/*
+ * 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.Nullable;
+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(@Nullable 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..dcf9a1b7
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TaggerImpl.java
@@ -0,0 +1,116 @@
+/*
+ * 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.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+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 java.util.Iterator;
+
+/** Implementation of {@link Tagger}. */
+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 CurrentState state;
+
+ TaggerImpl(CurrentState state) {
+ this.state = state;
+ }
+
+ @Override
+ public TagContextImpl empty() {
+ return TagContextImpl.EMPTY;
+ }
+
+ @Override
+ public TagContextImpl getCurrentTagContext() {
+ return state.getInternal() == State.DISABLED
+ ? TagContextImpl.EMPTY
+ : toTagContextImpl(CurrentTagContextUtils.getCurrentTagContext());
+ }
+
+ @Override
+ public TagContextBuilder emptyBuilder() {
+ return state.getInternal() == State.DISABLED
+ ? NoopTagContextBuilder.INSTANCE
+ : new TagContextBuilderImpl();
+ }
+
+ @Override
+ public TagContextBuilder currentBuilder() {
+ return state.getInternal() == State.DISABLED
+ ? NoopTagContextBuilder.INSTANCE
+ : toBuilder(CurrentTagContextUtils.getCurrentTagContext());
+ }
+
+ @Override
+ public TagContextBuilder toBuilder(TagContext tags) {
+ return state.getInternal() == State.DISABLED
+ ? NoopTagContextBuilder.INSTANCE
+ : toTagContextBuilderImpl(tags);
+ }
+
+ @Override
+ public Scope withTagContext(TagContext tags) {
+ return state.getInternal() == State.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..88c31bae
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.java
@@ -0,0 +1,68 @@
+/*
+ * 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.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+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 {
+ private static final State DEFAULT_STATE = State.ENABLED;
+
+ // The State shared between the TagsComponent, Tagger, and TagPropagationComponent
+ private final CurrentState currentState = new CurrentState(DEFAULT_STATE);
+
+ private final Tagger tagger = new TaggerImpl(currentState);
+ private final TagPropagationComponent tagPropagationComponent =
+ new TagPropagationComponentImpl(currentState);
+
+ @Override
+ public Tagger getTagger() {
+ return tagger;
+ }
+
+ @Override
+ public TagPropagationComponent getTagPropagationComponent() {
+ return tagPropagationComponent;
+ }
+
+ @Override
+ public TaggingState getState() {
+ return stateToTaggingState(currentState.get());
+ }
+
+ @Override
+ @Deprecated
+ public void setState(TaggingState newState) {
+ currentState.set(taggingStateToState(checkNotNull(newState, "newState")));
+ }
+
+ private static State taggingStateToState(TaggingState taggingState) {
+ return taggingState == TaggingState.ENABLED ? State.ENABLED : State.DISABLED;
+ }
+
+ private static TaggingState stateToTaggingState(State state) {
+ return state == State.ENABLED ? TaggingState.ENABLED : TaggingState.DISABLED;
+ }
+}
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..2daad95e
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java
@@ -0,0 +1,190 @@
+/*
+ * 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 io.opencensus.tags.propagation.TagContextSerializationException;
+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;
+ // This size limit only applies to the bytes representing tag keys and values.
+ @VisibleForTesting static final int TAGCONTEXT_SERIALIZED_SIZE_LIMIT = 8192;
+
+ // Serializes a TagContext to the on-the-wire format.
+ // Encoded tags are of the form: <version_id><encoded_tags>
+ static byte[] serializeBinary(TagContext tags) throws TagContextSerializationException {
+ // Use a ByteArrayDataOutput to avoid needing to handle IOExceptions.
+ final ByteArrayDataOutput byteArrayDataOutput = ByteStreams.newDataOutput();
+ byteArrayDataOutput.write(VERSION_ID);
+ int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
+ for (Iterator<Tag> i = InternalUtils.getTags(tags); i.hasNext(); ) {
+ Tag tag = i.next();
+ totalChars += tag.getKey().getName().length();
+ totalChars += tag.getValue().asString().length();
+ encodeTag(tag, byteArrayDataOutput);
+ }
+ if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) {
+ throw new TagContextSerializationException(
+ "Size of TagContext exceeds the maximum serialized size "
+ + TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
+ }
+ 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 || versionId < 0) {
+ throw new TagContextDeserializationException(
+ "Wrong Version ID: " + versionId + ". Currently supports version up to: " + 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();
+ int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
+ while (buffer.position() < limit) {
+ int type = buffer.get();
+ if (type == TAG_FIELD_ID) {
+ TagKey key = createTagKey(decodeString(buffer));
+ TagValue val = createTagValue(key, decodeString(buffer));
+ totalChars += key.getName().length();
+ totalChars += val.asString().length();
+ 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.
+ break;
+ }
+ }
+ if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) {
+ throw new TagContextDeserializationException(
+ "Size of TagContext exceeds the maximum serialized size "
+ + TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
+ }
+ 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..5a25da5b
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.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.propagation;
+
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+import io.opencensus.implcore.tags.TagContextImpl;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+
+final class TagContextBinarySerializerImpl extends TagContextBinarySerializer {
+ private static final byte[] EMPTY_BYTE_ARRAY = {};
+
+ private final CurrentState state;
+
+ TagContextBinarySerializerImpl(CurrentState state) {
+ this.state = state;
+ }
+
+ @Override
+ public byte[] toByteArray(TagContext tags) throws TagContextSerializationException {
+ return state.getInternal() == State.DISABLED
+ ? EMPTY_BYTE_ARRAY
+ : SerializationUtils.serializeBinary(tags);
+ }
+
+ @Override
+ public TagContext fromByteArray(byte[] bytes) throws TagContextDeserializationException {
+ return state.getInternal() == State.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..9ba0da40
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java
@@ -0,0 +1,35 @@
+/*
+ * 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.internal.CurrentState;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+
+/** Implementation of {@link TagPropagationComponent}. */
+public final class TagPropagationComponentImpl extends TagPropagationComponent {
+ private final TagContextBinarySerializer tagContextBinarySerializer;
+
+ public TagPropagationComponentImpl(CurrentState state) {
+ tagContextBinarySerializer = new TagContextBinarySerializerImpl(state);
+ }
+
+ @Override
+ public TagContextBinarySerializer getBinarySerializer() {
+ return tagContextBinarySerializer;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/NoRecordEventsSpanImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/NoRecordEventsSpanImpl.java
new file mode 100644
index 00000000..8a5f8e05
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/NoRecordEventsSpanImpl.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import com.google.common.base.Preconditions;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Status;
+import java.util.EnumSet;
+import java.util.Map;
+
+/** Implementation for the {@link Span} class that does not record trace events. */
+final class NoRecordEventsSpanImpl extends Span {
+
+ private static final EnumSet<Options> NOT_RECORD_EVENTS_SPAN_OPTIONS =
+ EnumSet.noneOf(Span.Options.class);
+
+ static NoRecordEventsSpanImpl create(SpanContext context) {
+ return new NoRecordEventsSpanImpl(context);
+ }
+
+ @Override
+ public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+ Preconditions.checkNotNull(description, "description");
+ Preconditions.checkNotNull(attributes, "attribute");
+ }
+
+ @Override
+ public void addAnnotation(Annotation annotation) {
+ Preconditions.checkNotNull(annotation, "annotation");
+ }
+
+ @Override
+ public void putAttribute(String key, AttributeValue value) {
+ Preconditions.checkNotNull(key, "key");
+ Preconditions.checkNotNull(value, "value");
+ }
+
+ @Override
+ public void putAttributes(Map<String, AttributeValue> attributes) {
+ Preconditions.checkNotNull(attributes, "attributes");
+ }
+
+ @Override
+ public void addMessageEvent(io.opencensus.trace.MessageEvent messageEvent) {
+ Preconditions.checkNotNull(messageEvent, "messageEvent");
+ }
+
+ @Override
+ public void addLink(Link link) {
+ Preconditions.checkNotNull(link, "link");
+ }
+
+ @Override
+ public void setStatus(Status status) {
+ Preconditions.checkNotNull(status, "status");
+ }
+
+ @Override
+ public void end(EndSpanOptions options) {
+ Preconditions.checkNotNull(options, "options");
+ }
+
+ private NoRecordEventsSpanImpl(SpanContext context) {
+ super(context, NOT_RECORD_EVENTS_SPAN_OPTIONS);
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/RecordEventsSpanImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/RecordEventsSpanImpl.java
new file mode 100644
index 00000000..af3545bc
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/RecordEventsSpanImpl.java
@@ -0,0 +1,579 @@
+/*
+ * 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.trace;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.EvictingQueue;
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.CheckerFrameworkUtils;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.internal.ConcurrentIntrusiveList.Element;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanData.TimedEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+// TODO(hailongwen): remove the usage of `NetworkEvent` in the future.
+/** Implementation for the {@link Span} class that records trace events. */
+@ThreadSafe
+public final class RecordEventsSpanImpl extends Span implements Element<RecordEventsSpanImpl> {
+ private static final Logger logger = Logger.getLogger(Tracer.class.getName());
+
+ private static final EnumSet<Span.Options> RECORD_EVENTS_SPAN_OPTIONS =
+ EnumSet.of(Span.Options.RECORD_EVENTS);
+
+ // The parent SpanId of this span. Null if this is a root span.
+ @Nullable private final SpanId parentSpanId;
+ // True if the parent is on a different process.
+ @Nullable private final Boolean hasRemoteParent;
+ // Active trace params when the Span was created.
+ private final TraceParams traceParams;
+ // Handler called when the span starts and ends.
+ private final StartEndHandler startEndHandler;
+ // The displayed name of the span.
+ private final String name;
+ // The kind of the span.
+ @Nullable private final Kind kind;
+ // The clock used to get the time.
+ private final Clock clock;
+ // The time converter used to convert nano time to Timestamp. This is needed because Java has
+ // millisecond granularity for Timestamp and tracing events are recorded more often.
+ @Nullable private final TimestampConverter timestampConverter;
+ // The start time of the span.
+ private final long startNanoTime;
+ // Set of recorded attributes. DO NOT CALL any other method that changes the ordering of events.
+ @GuardedBy("this")
+ @Nullable
+ private AttributesWithCapacity attributes;
+ // List of recorded annotations.
+ @GuardedBy("this")
+ @Nullable
+ private TraceEvents<EventWithNanoTime<Annotation>> annotations;
+ // List of recorded network events.
+ @GuardedBy("this")
+ @Nullable
+ private TraceEvents<EventWithNanoTime<io.opencensus.trace.MessageEvent>> messageEvents;
+ // List of recorded links to parent and child spans.
+ @GuardedBy("this")
+ @Nullable
+ private TraceEvents<Link> links;
+ // The status of the span.
+ @GuardedBy("this")
+ @Nullable
+ private Status status;
+ // The end time of the span.
+ @GuardedBy("this")
+ private long endNanoTime;
+ // True if the span is ended.
+ @GuardedBy("this")
+ private boolean hasBeenEnded;
+
+ @GuardedBy("this")
+ private boolean sampleToLocalSpanStore;
+
+ // Pointers for the ConcurrentIntrusiveList$Element. Guarded by the ConcurrentIntrusiveList.
+ @Nullable private RecordEventsSpanImpl next = null;
+ @Nullable private RecordEventsSpanImpl prev = null;
+
+ /**
+ * Creates and starts a span with the given configuration.
+ *
+ * @param context supplies the trace_id and span_id for the newly started span.
+ * @param name the displayed name for the new span.
+ * @param parentSpanId the span_id of the parent span, or null if the new span is a root span.
+ * @param hasRemoteParent {@code true} if the parentContext is remote. {@code null} if this is a
+ * root span.
+ * @param traceParams trace parameters like sampler and probability.
+ * @param startEndHandler handler called when the span starts and ends.
+ * @param timestampConverter null if the span is a root span or the parent is not sampled. If the
+ * parent is sampled, we should use the same converter to ensure ordering between tracing
+ * events.
+ * @param clock the clock used to get the time.
+ * @return a new and started span.
+ */
+ @VisibleForTesting
+ public static RecordEventsSpanImpl startSpan(
+ SpanContext context,
+ String name,
+ @Nullable Kind kind,
+ @Nullable SpanId parentSpanId,
+ @Nullable Boolean hasRemoteParent,
+ TraceParams traceParams,
+ StartEndHandler startEndHandler,
+ @Nullable TimestampConverter timestampConverter,
+ Clock clock) {
+ RecordEventsSpanImpl span =
+ new RecordEventsSpanImpl(
+ context,
+ name,
+ kind,
+ parentSpanId,
+ hasRemoteParent,
+ traceParams,
+ startEndHandler,
+ timestampConverter,
+ clock);
+ // Call onStart here instead of calling in the constructor to make sure the span is completely
+ // initialized.
+ startEndHandler.onStart(span);
+ return span;
+ }
+
+ /**
+ * Returns the name of the {@code Span}.
+ *
+ * @return the name of the {@code Span}.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the status of the {@code Span}. If not set defaults to {@link Status#OK}.
+ *
+ * @return the status of the {@code Span}.
+ */
+ public Status getStatus() {
+ synchronized (this) {
+ return getStatusWithDefault();
+ }
+ }
+
+ /**
+ * Returns the end nano time (see {@link System#nanoTime()}). If the current {@code Span} is not
+ * ended then returns {@link Clock#nowNanos()}.
+ *
+ * @return the end nano time.
+ */
+ public long getEndNanoTime() {
+ synchronized (this) {
+ return hasBeenEnded ? endNanoTime : clock.nowNanos();
+ }
+ }
+
+ /**
+ * Returns the latency of the {@code Span} in nanos. If still active then returns now() - start
+ * time.
+ *
+ * @return the latency of the {@code Span} in nanos.
+ */
+ public long getLatencyNs() {
+ synchronized (this) {
+ return hasBeenEnded ? endNanoTime - startNanoTime : clock.nowNanos() - startNanoTime;
+ }
+ }
+
+ /**
+ * Returns if the name of this {@code Span} must be register to the {@code SampledSpanStore}.
+ *
+ * @return if the name of this {@code Span} must be register to the {@code SampledSpanStore}.
+ */
+ public boolean getSampleToLocalSpanStore() {
+ synchronized (this) {
+ checkState(hasBeenEnded, "Running span does not have the SampleToLocalSpanStore set.");
+ return sampleToLocalSpanStore;
+ }
+ }
+
+ /**
+ * Returns the kind of this {@code Span}.
+ *
+ * @return the kind of this {@code Span}.
+ */
+ @Nullable
+ public Kind getKind() {
+ return kind;
+ }
+
+ /**
+ * Returns the {@code TimestampConverter} used by this {@code Span}.
+ *
+ * @return the {@code TimestampConverter} used by this {@code Span}.
+ */
+ @Nullable
+ TimestampConverter getTimestampConverter() {
+ return timestampConverter;
+ }
+
+ /**
+ * Returns an immutable representation of all the data from this {@code Span}.
+ *
+ * @return an immutable representation of all the data from this {@code Span}.
+ * @throws IllegalStateException if the Span doesn't have RECORD_EVENTS option.
+ */
+ public SpanData toSpanData() {
+ synchronized (this) {
+ SpanData.Attributes attributesSpanData =
+ attributes == null
+ ? SpanData.Attributes.create(Collections.<String, AttributeValue>emptyMap(), 0)
+ : SpanData.Attributes.create(attributes, attributes.getNumberOfDroppedAttributes());
+ SpanData.TimedEvents<Annotation> annotationsSpanData =
+ createTimedEvents(getInitializedAnnotations(), timestampConverter);
+ SpanData.TimedEvents<io.opencensus.trace.MessageEvent> messageEventsSpanData =
+ createTimedEvents(getInitializedNetworkEvents(), timestampConverter);
+ SpanData.Links linksSpanData =
+ links == null
+ ? SpanData.Links.create(Collections.<Link>emptyList(), 0)
+ : SpanData.Links.create(
+ new ArrayList<Link>(links.events), links.getNumberOfDroppedEvents());
+ return SpanData.create(
+ getContext(),
+ parentSpanId,
+ hasRemoteParent,
+ name,
+ kind,
+ CheckerFrameworkUtils.castNonNull(timestampConverter).convertNanoTime(startNanoTime),
+ attributesSpanData,
+ annotationsSpanData,
+ messageEventsSpanData,
+ linksSpanData,
+ null, // Not supported yet.
+ hasBeenEnded ? getStatusWithDefault() : null,
+ hasBeenEnded
+ ? CheckerFrameworkUtils.castNonNull(timestampConverter).convertNanoTime(endNanoTime)
+ : null);
+ }
+ }
+
+ @Override
+ public void putAttribute(String key, AttributeValue value) {
+ Preconditions.checkNotNull(key, "key");
+ Preconditions.checkNotNull(value, "value");
+ synchronized (this) {
+ if (hasBeenEnded) {
+ logger.log(Level.FINE, "Calling putAttributes() on an ended Span.");
+ return;
+ }
+ getInitializedAttributes().putAttribute(key, value);
+ }
+ }
+
+ @Override
+ public void putAttributes(Map<String, AttributeValue> attributes) {
+ Preconditions.checkNotNull(attributes, "attributes");
+ synchronized (this) {
+ if (hasBeenEnded) {
+ logger.log(Level.FINE, "Calling putAttributes() on an ended Span.");
+ return;
+ }
+ getInitializedAttributes().putAttributes(attributes);
+ }
+ }
+
+ @Override
+ public void addAnnotation(String description, Map<String, AttributeValue> attributes) {
+ Preconditions.checkNotNull(description, "description");
+ Preconditions.checkNotNull(attributes, "attribute");
+ synchronized (this) {
+ if (hasBeenEnded) {
+ logger.log(Level.FINE, "Calling addAnnotation() on an ended Span.");
+ return;
+ }
+ getInitializedAnnotations()
+ .addEvent(
+ new EventWithNanoTime<Annotation>(
+ clock.nowNanos(),
+ Annotation.fromDescriptionAndAttributes(description, attributes)));
+ }
+ }
+
+ @Override
+ public void addAnnotation(Annotation annotation) {
+ Preconditions.checkNotNull(annotation, "annotation");
+ synchronized (this) {
+ if (hasBeenEnded) {
+ logger.log(Level.FINE, "Calling addAnnotation() on an ended Span.");
+ return;
+ }
+ getInitializedAnnotations()
+ .addEvent(new EventWithNanoTime<Annotation>(clock.nowNanos(), annotation));
+ }
+ }
+
+ @Override
+ public void addMessageEvent(io.opencensus.trace.MessageEvent messageEvent) {
+ Preconditions.checkNotNull(messageEvent, "messageEvent");
+ synchronized (this) {
+ if (hasBeenEnded) {
+ logger.log(Level.FINE, "Calling addNetworkEvent() on an ended Span.");
+ return;
+ }
+ getInitializedNetworkEvents()
+ .addEvent(
+ new EventWithNanoTime<io.opencensus.trace.MessageEvent>(
+ clock.nowNanos(), checkNotNull(messageEvent, "networkEvent")));
+ }
+ }
+
+ @Override
+ public void addLink(Link link) {
+ Preconditions.checkNotNull(link, "link");
+ synchronized (this) {
+ if (hasBeenEnded) {
+ logger.log(Level.FINE, "Calling addLink() on an ended Span.");
+ return;
+ }
+ getInitializedLinks().addEvent(link);
+ }
+ }
+
+ @Override
+ public void setStatus(Status status) {
+ Preconditions.checkNotNull(status, "status");
+ synchronized (this) {
+ if (hasBeenEnded) {
+ logger.log(Level.FINE, "Calling setStatus() on an ended Span.");
+ return;
+ }
+ this.status = status;
+ }
+ }
+
+ @Override
+ public void end(EndSpanOptions options) {
+ Preconditions.checkNotNull(options, "options");
+ synchronized (this) {
+ if (hasBeenEnded) {
+ logger.log(Level.FINE, "Calling end() on an ended Span.");
+ return;
+ }
+ if (options.getStatus() != null) {
+ status = options.getStatus();
+ }
+ sampleToLocalSpanStore = options.getSampleToLocalSpanStore();
+ endNanoTime = clock.nowNanos();
+ hasBeenEnded = true;
+ }
+ startEndHandler.onEnd(this);
+ }
+
+ @GuardedBy("this")
+ private AttributesWithCapacity getInitializedAttributes() {
+ if (attributes == null) {
+ attributes = new AttributesWithCapacity(traceParams.getMaxNumberOfAttributes());
+ }
+ return attributes;
+ }
+
+ @GuardedBy("this")
+ private TraceEvents<EventWithNanoTime<Annotation>> getInitializedAnnotations() {
+ if (annotations == null) {
+ annotations =
+ new TraceEvents<EventWithNanoTime<Annotation>>(traceParams.getMaxNumberOfAnnotations());
+ }
+ return annotations;
+ }
+
+ @GuardedBy("this")
+ private TraceEvents<EventWithNanoTime<io.opencensus.trace.MessageEvent>>
+ getInitializedNetworkEvents() {
+ if (messageEvents == null) {
+ messageEvents =
+ new TraceEvents<EventWithNanoTime<io.opencensus.trace.MessageEvent>>(
+ traceParams.getMaxNumberOfMessageEvents());
+ }
+ return messageEvents;
+ }
+
+ @GuardedBy("this")
+ private TraceEvents<Link> getInitializedLinks() {
+ if (links == null) {
+ links = new TraceEvents<Link>(traceParams.getMaxNumberOfLinks());
+ }
+ return links;
+ }
+
+ @GuardedBy("this")
+ private Status getStatusWithDefault() {
+ return status == null ? Status.OK : status;
+ }
+
+ private static <T> SpanData.TimedEvents<T> createTimedEvents(
+ TraceEvents<EventWithNanoTime<T>> events, @Nullable TimestampConverter timestampConverter) {
+ if (events == null) {
+ return SpanData.TimedEvents.create(Collections.<TimedEvent<T>>emptyList(), 0);
+ }
+ List<TimedEvent<T>> eventsList = new ArrayList<TimedEvent<T>>(events.events.size());
+ for (EventWithNanoTime<T> networkEvent : events.events) {
+ eventsList.add(
+ networkEvent.toSpanDataTimedEvent(CheckerFrameworkUtils.castNonNull(timestampConverter)));
+ }
+ return SpanData.TimedEvents.create(eventsList, events.getNumberOfDroppedEvents());
+ }
+
+ @Override
+ @Nullable
+ public RecordEventsSpanImpl getNext() {
+ return next;
+ }
+
+ @Override
+ public void setNext(@Nullable RecordEventsSpanImpl element) {
+ next = element;
+ }
+
+ @Override
+ @Nullable
+ public RecordEventsSpanImpl getPrev() {
+ return prev;
+ }
+
+ @Override
+ public void setPrev(@Nullable RecordEventsSpanImpl element) {
+ prev = element;
+ }
+
+ /**
+ * Interface to handle the start and end operations for a {@link Span} only when the {@code Span}
+ * has {@link Options#RECORD_EVENTS} option.
+ *
+ * <p>Implementation must avoid high overhead work in any of the methods because the code is
+ * executed on the critical path.
+ *
+ * <p>One instance can be called by multiple threads in the same time, so the implementation must
+ * be thread-safe.
+ */
+ public interface StartEndHandler {
+ void onStart(RecordEventsSpanImpl span);
+
+ void onEnd(RecordEventsSpanImpl span);
+ }
+
+ // A map implementation with a fixed capacity that drops events when the map gets full. Eviction
+ // is based on the access order.
+ private static final class AttributesWithCapacity extends LinkedHashMap<String, AttributeValue> {
+ private final int capacity;
+ private int totalRecordedAttributes = 0;
+ // Here because -Werror complains about this: [serial] serializable class AttributesWithCapacity
+ // has no definition of serialVersionUID. This class shouldn't be serialized.
+ private static final long serialVersionUID = 42L;
+
+ private AttributesWithCapacity(int capacity) {
+ // Capacity of the map is capacity + 1 to avoid resizing because removeEldestEntry is invoked
+ // by put and putAll after inserting a new entry into the map. The loadFactor is set to 1
+ // to avoid resizing because. The accessOrder is set to true.
+ super(capacity + 1, 1, /*accessOrder=*/ true);
+ this.capacity = capacity;
+ }
+
+ // Users must call this method instead of put to keep count of the total number of entries
+ // inserted.
+ private void putAttribute(String key, AttributeValue value) {
+ totalRecordedAttributes += 1;
+ put(key, value);
+ }
+
+ // Users must call this method instead of putAll to keep count of the total number of entries
+ // inserted.
+ private void putAttributes(Map<String, AttributeValue> attributes) {
+ totalRecordedAttributes += attributes.size();
+ putAll(attributes);
+ }
+
+ private int getNumberOfDroppedAttributes() {
+ return totalRecordedAttributes - size();
+ }
+
+ // It is called after each put or putAll call in order to determine if the eldest inserted
+ // entry should be removed or not.
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<String, AttributeValue> eldest) {
+ return size() > this.capacity;
+ }
+ }
+
+ private static final class TraceEvents<T> {
+ private int totalRecordedEvents = 0;
+ private final EvictingQueue<T> events;
+
+ private int getNumberOfDroppedEvents() {
+ return totalRecordedEvents - events.size();
+ }
+
+ TraceEvents(int maxNumEvents) {
+ events = EvictingQueue.create(maxNumEvents);
+ }
+
+ void addEvent(T event) {
+ totalRecordedEvents++;
+ events.add(event);
+ }
+ }
+
+ // Timed event that uses nanoTime to represent the Timestamp.
+ private static final class EventWithNanoTime<T> {
+ private final long nanoTime;
+ private final T event;
+
+ private EventWithNanoTime(long nanoTime, T event) {
+ this.nanoTime = nanoTime;
+ this.event = event;
+ }
+
+ private TimedEvent<T> toSpanDataTimedEvent(TimestampConverter timestampConverter) {
+ return TimedEvent.create(timestampConverter.convertNanoTime(nanoTime), event);
+ }
+ }
+
+ private RecordEventsSpanImpl(
+ SpanContext context,
+ String name,
+ @Nullable Kind kind,
+ @Nullable SpanId parentSpanId,
+ @Nullable Boolean hasRemoteParent,
+ TraceParams traceParams,
+ StartEndHandler startEndHandler,
+ @Nullable TimestampConverter timestampConverter,
+ Clock clock) {
+ super(context, RECORD_EVENTS_SPAN_OPTIONS);
+ this.parentSpanId = parentSpanId;
+ this.hasRemoteParent = hasRemoteParent;
+ this.name = name;
+ this.kind = kind;
+ this.traceParams = traceParams;
+ this.startEndHandler = startEndHandler;
+ this.clock = clock;
+ this.hasBeenEnded = false;
+ this.sampleToLocalSpanStore = false;
+ this.timestampConverter =
+ timestampConverter != null ? timestampConverter : TimestampConverter.now(clock);
+ startNanoTime = clock.nowNanos();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/SpanBuilderImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/SpanBuilderImpl.java
new file mode 100644
index 00000000..5565e9de
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/SpanBuilderImpl.java
@@ -0,0 +1,253 @@
+/*
+ * 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.trace;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.Link.Type;
+import io.opencensus.trace.Sampler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import javax.annotation.Nullable;
+
+/** Implementation of the {@link SpanBuilder}. */
+final class SpanBuilderImpl extends SpanBuilder {
+ private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+
+ private static final TraceOptions SAMPLED_TRACE_OPTIONS =
+ TraceOptions.builder().setIsSampled(true).build();
+ private static final TraceOptions NOT_SAMPLED_TRACE_OPTIONS =
+ TraceOptions.builder().setIsSampled(false).build();
+
+ private final Options options;
+ private final String name;
+ @Nullable private final Span parent;
+ @Nullable private final SpanContext remoteParentSpanContext;
+ @Nullable private Sampler sampler;
+ private List<Span> parentLinks = Collections.<Span>emptyList();
+ @Nullable private Boolean recordEvents;
+ @Nullable private Kind kind;
+
+ private Span startSpanInternal(
+ @Nullable SpanContext parent,
+ @Nullable Boolean hasRemoteParent,
+ String name,
+ @Nullable Sampler sampler,
+ List<Span> parentLinks,
+ @Nullable Boolean recordEvents,
+ @Nullable Kind kind,
+ @Nullable TimestampConverter timestampConverter) {
+ TraceParams activeTraceParams = options.traceConfig.getActiveTraceParams();
+ Random random = options.randomHandler.current();
+ TraceId traceId;
+ SpanId spanId = SpanId.generateRandomId(random);
+ SpanId parentSpanId = null;
+ // TODO(bdrutu): Handle tracestate correctly not just propagate.
+ Tracestate tracestate = TRACESTATE_DEFAULT;
+ if (parent == null || !parent.isValid()) {
+ // New root span.
+ traceId = TraceId.generateRandomId(random);
+ // This is a root span so no remote or local parent.
+ hasRemoteParent = null;
+ } else {
+ // New child span.
+ traceId = parent.getTraceId();
+ parentSpanId = parent.getSpanId();
+ tracestate = parent.getTracestate();
+ }
+ TraceOptions traceOptions =
+ makeSamplingDecision(
+ parent,
+ hasRemoteParent,
+ name,
+ sampler,
+ parentLinks,
+ traceId,
+ spanId,
+ activeTraceParams)
+ ? SAMPLED_TRACE_OPTIONS
+ : NOT_SAMPLED_TRACE_OPTIONS;
+ Span span =
+ (traceOptions.isSampled() || Boolean.TRUE.equals(recordEvents))
+ ? RecordEventsSpanImpl.startSpan(
+ SpanContext.create(traceId, spanId, traceOptions, tracestate),
+ name,
+ kind,
+ parentSpanId,
+ hasRemoteParent,
+ activeTraceParams,
+ options.startEndHandler,
+ timestampConverter,
+ options.clock)
+ : NoRecordEventsSpanImpl.create(
+ SpanContext.create(traceId, spanId, traceOptions, tracestate));
+ linkSpans(span, parentLinks);
+ return span;
+ }
+
+ private static boolean makeSamplingDecision(
+ @Nullable SpanContext parent,
+ @Nullable Boolean hasRemoteParent,
+ String name,
+ @Nullable Sampler sampler,
+ List<Span> parentLinks,
+ TraceId traceId,
+ SpanId spanId,
+ TraceParams activeTraceParams) {
+ // If users set a specific sampler in the SpanBuilder, use it.
+ if (sampler != null) {
+ return sampler.shouldSample(parent, hasRemoteParent, traceId, spanId, name, parentLinks);
+ }
+ // Use the default sampler if this is a root Span or this is an entry point Span (has remote
+ // parent).
+ if (Boolean.TRUE.equals(hasRemoteParent) || parent == null || !parent.isValid()) {
+ return activeTraceParams
+ .getSampler()
+ .shouldSample(parent, hasRemoteParent, traceId, spanId, name, parentLinks);
+ }
+ // Parent is always different than null because otherwise we use the default sampler.
+ return parent.getTraceOptions().isSampled() || isAnyParentLinkSampled(parentLinks);
+ }
+
+ private static boolean isAnyParentLinkSampled(List<Span> parentLinks) {
+ for (Span parentLink : parentLinks) {
+ if (parentLink.getContext().getTraceOptions().isSampled()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static void linkSpans(Span span, List<Span> parentLinks) {
+ if (!parentLinks.isEmpty()) {
+ Link childLink = Link.fromSpanContext(span.getContext(), Type.CHILD_LINKED_SPAN);
+ for (Span linkedSpan : parentLinks) {
+ linkedSpan.addLink(childLink);
+ span.addLink(Link.fromSpanContext(linkedSpan.getContext(), Type.PARENT_LINKED_SPAN));
+ }
+ }
+ }
+
+ static SpanBuilderImpl createWithParent(String spanName, @Nullable Span parent, Options options) {
+ return new SpanBuilderImpl(spanName, null, parent, options);
+ }
+
+ static SpanBuilderImpl createWithRemoteParent(
+ String spanName, @Nullable SpanContext remoteParentSpanContext, Options options) {
+ return new SpanBuilderImpl(spanName, remoteParentSpanContext, null, options);
+ }
+
+ private SpanBuilderImpl(
+ String name,
+ @Nullable SpanContext remoteParentSpanContext,
+ @Nullable Span parent,
+ Options options) {
+ this.name = checkNotNull(name, "name");
+ this.parent = parent;
+ this.remoteParentSpanContext = remoteParentSpanContext;
+ this.options = options;
+ }
+
+ @Override
+ public Span startSpan() {
+ SpanContext parentContext = remoteParentSpanContext;
+ Boolean hasRemoteParent = Boolean.TRUE;
+ TimestampConverter timestampConverter = null;
+ if (remoteParentSpanContext == null) {
+ // This is not a child of a remote Span. Get the parent SpanContext from the parent Span if
+ // any.
+ Span parent = this.parent;
+ hasRemoteParent = Boolean.FALSE;
+ if (parent != null) {
+ parentContext = parent.getContext();
+ // Pass the timestamp converter from the parent to ensure that the recorded events are in
+ // the right order. Implementation uses System.nanoTime() which is monotonically increasing.
+ if (parent instanceof RecordEventsSpanImpl) {
+ timestampConverter = ((RecordEventsSpanImpl) parent).getTimestampConverter();
+ }
+ } else {
+ hasRemoteParent = null;
+ }
+ }
+ return startSpanInternal(
+ parentContext,
+ hasRemoteParent,
+ name,
+ sampler,
+ parentLinks,
+ recordEvents,
+ kind,
+ timestampConverter);
+ }
+
+ static final class Options {
+ private final RandomHandler randomHandler;
+ private final RecordEventsSpanImpl.StartEndHandler startEndHandler;
+ private final Clock clock;
+ private final TraceConfig traceConfig;
+
+ Options(
+ RandomHandler randomHandler,
+ RecordEventsSpanImpl.StartEndHandler startEndHandler,
+ Clock clock,
+ TraceConfig traceConfig) {
+ this.randomHandler = checkNotNull(randomHandler, "randomHandler");
+ this.startEndHandler = checkNotNull(startEndHandler, "startEndHandler");
+ this.clock = checkNotNull(clock, "clock");
+ this.traceConfig = checkNotNull(traceConfig, "traceConfig");
+ }
+ }
+
+ @Override
+ public SpanBuilderImpl setSampler(Sampler sampler) {
+ this.sampler = checkNotNull(sampler, "sampler");
+ return this;
+ }
+
+ @Override
+ public SpanBuilderImpl setParentLinks(List<Span> parentLinks) {
+ this.parentLinks = checkNotNull(parentLinks, "parentLinks");
+ return this;
+ }
+
+ @Override
+ public SpanBuilderImpl setRecordEvents(boolean recordEvents) {
+ this.recordEvents = recordEvents;
+ return this;
+ }
+
+ @Override
+ public SpanBuilderImpl setSpanKind(@Nullable Kind kind) {
+ this.kind = kind;
+ return this;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/StartEndHandlerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/StartEndHandlerImpl.java
new file mode 100644
index 00000000..6adaa200
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/StartEndHandlerImpl.java
@@ -0,0 +1,127 @@
+/*
+ * 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.trace;
+
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.export.RunningSpanStoreImpl;
+import io.opencensus.implcore.trace.export.SampledSpanStoreImpl;
+import io.opencensus.implcore.trace.export.SpanExporterImpl;
+import io.opencensus.trace.Span.Options;
+import io.opencensus.trace.export.SpanData;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Uses the provided {@link EventQueue} to defer processing/exporting of the {@link SpanData} to
+ * avoid impacting the critical path.
+ */
+@ThreadSafe
+public final class StartEndHandlerImpl implements StartEndHandler {
+ private final SpanExporterImpl spanExporter;
+ @Nullable private final RunningSpanStoreImpl runningSpanStore;
+ @Nullable private final SampledSpanStoreImpl sampledSpanStore;
+ private final EventQueue eventQueue;
+ // true if any of (runningSpanStore OR sampledSpanStore) are different than null, which
+ // means the spans with RECORD_EVENTS should be enqueued in the queue.
+ private final boolean enqueueEventForNonSampledSpans;
+
+ /**
+ * Constructs a new {@code StartEndHandlerImpl}.
+ *
+ * @param spanExporter the {@code SpanExporter} implementation.
+ * @param runningSpanStore the {@code RunningSpanStore} implementation.
+ * @param sampledSpanStore the {@code SampledSpanStore} implementation.
+ * @param eventQueue the event queue where all the events are enqueued.
+ */
+ public StartEndHandlerImpl(
+ SpanExporterImpl spanExporter,
+ @Nullable RunningSpanStoreImpl runningSpanStore,
+ @Nullable SampledSpanStoreImpl sampledSpanStore,
+ EventQueue eventQueue) {
+ this.spanExporter = spanExporter;
+ this.runningSpanStore = runningSpanStore;
+ this.sampledSpanStore = sampledSpanStore;
+ this.enqueueEventForNonSampledSpans = runningSpanStore != null || sampledSpanStore != null;
+ this.eventQueue = eventQueue;
+ }
+
+ @Override
+ public void onStart(RecordEventsSpanImpl span) {
+ if (span.getOptions().contains(Options.RECORD_EVENTS) && enqueueEventForNonSampledSpans) {
+ eventQueue.enqueue(new SpanStartEvent(span, runningSpanStore));
+ }
+ }
+
+ @Override
+ public void onEnd(RecordEventsSpanImpl span) {
+ if ((span.getOptions().contains(Options.RECORD_EVENTS) && enqueueEventForNonSampledSpans)
+ || span.getContext().getTraceOptions().isSampled()) {
+ eventQueue.enqueue(new SpanEndEvent(span, spanExporter, runningSpanStore, sampledSpanStore));
+ }
+ }
+
+ // An EventQueue entry that records the start of the span event.
+ private static final class SpanStartEvent implements EventQueue.Entry {
+ private final RecordEventsSpanImpl span;
+ @Nullable private final RunningSpanStoreImpl activeSpansExporter;
+
+ SpanStartEvent(RecordEventsSpanImpl span, @Nullable RunningSpanStoreImpl activeSpansExporter) {
+ this.span = span;
+ this.activeSpansExporter = activeSpansExporter;
+ }
+
+ @Override
+ public void process() {
+ if (activeSpansExporter != null) {
+ activeSpansExporter.onStart(span);
+ }
+ }
+ }
+
+ // An EventQueue entry that records the end of the span event.
+ private static final class SpanEndEvent implements EventQueue.Entry {
+ private final RecordEventsSpanImpl span;
+ @Nullable private final RunningSpanStoreImpl runningSpanStore;
+ private final SpanExporterImpl spanExporter;
+ @Nullable private final SampledSpanStoreImpl sampledSpanStore;
+
+ SpanEndEvent(
+ RecordEventsSpanImpl span,
+ SpanExporterImpl spanExporter,
+ @Nullable RunningSpanStoreImpl runningSpanStore,
+ @Nullable SampledSpanStoreImpl sampledSpanStore) {
+ this.span = span;
+ this.runningSpanStore = runningSpanStore;
+ this.spanExporter = spanExporter;
+ this.sampledSpanStore = sampledSpanStore;
+ }
+
+ @Override
+ public void process() {
+ if (span.getContext().getTraceOptions().isSampled()) {
+ spanExporter.addSpan(span);
+ }
+ if (runningSpanStore != null) {
+ runningSpanStore.onEnd(span);
+ }
+ if (sampledSpanStore != null) {
+ sampledSpanStore.considerForSampling(span);
+ }
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/TraceComponentImplBase.java b/impl_core/src/main/java/io/opencensus/implcore/trace/TraceComponentImplBase.java
new file mode 100644
index 00000000..c1432432
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/TraceComponentImplBase.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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.config.TraceConfigImpl;
+import io.opencensus.implcore.trace.export.ExportComponentImpl;
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import io.opencensus.implcore.trace.propagation.PropagationComponentImpl;
+import io.opencensus.trace.TraceComponent;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/**
+ * Helper class to allow sharing the code for all the {@link TraceComponent} implementations. This
+ * class cannot use inheritance because in version 0.5.* the constructor of the {@code
+ * TraceComponent} is package protected.
+ *
+ * <p>This can be changed back to inheritance when version 0.5.* is no longer supported.
+ */
+public final class TraceComponentImplBase {
+ private final ExportComponentImpl exportComponent;
+ private final PropagationComponent propagationComponent = new PropagationComponentImpl();
+ private final Clock clock;
+ private final TraceConfig traceConfig = new TraceConfigImpl();
+ private final Tracer tracer;
+
+ /**
+ * Creates a new {@code TraceComponentImplBase}.
+ *
+ * @param clock the clock to use throughout tracing.
+ * @param randomHandler the random number generator for generating trace and span IDs.
+ * @param eventQueue the queue implementation.
+ */
+ public TraceComponentImplBase(Clock clock, RandomHandler randomHandler, EventQueue eventQueue) {
+ this.clock = clock;
+ // TODO(bdrutu): Add a config/argument for supportInProcessStores.
+ if (eventQueue instanceof SimpleEventQueue) {
+ exportComponent = ExportComponentImpl.createWithoutInProcessStores(eventQueue);
+ } else {
+ exportComponent = ExportComponentImpl.createWithInProcessStores(eventQueue);
+ }
+ StartEndHandler startEndHandler =
+ new StartEndHandlerImpl(
+ exportComponent.getSpanExporter(),
+ exportComponent.getRunningSpanStore(),
+ exportComponent.getSampledSpanStore(),
+ eventQueue);
+ tracer = new TracerImpl(randomHandler, startEndHandler, clock, traceConfig);
+ }
+
+ public Tracer getTracer() {
+ return tracer;
+ }
+
+ public PropagationComponent getPropagationComponent() {
+ return propagationComponent;
+ }
+
+ public final Clock getClock() {
+ return clock;
+ }
+
+ public ExportComponent getExportComponent() {
+ return exportComponent;
+ }
+
+ public TraceConfig getTraceConfig() {
+ return traceConfig;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/TracerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/TracerImpl.java
new file mode 100644
index 00000000..48df8055
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/TracerImpl.java
@@ -0,0 +1,52 @@
+/*
+ * 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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceConfig;
+import javax.annotation.Nullable;
+
+/** Implementation of the {@link Tracer}. */
+public final class TracerImpl extends Tracer {
+ private final SpanBuilderImpl.Options spanBuilderOptions;
+
+ TracerImpl(
+ RandomHandler randomHandler,
+ RecordEventsSpanImpl.StartEndHandler startEndHandler,
+ Clock clock,
+ TraceConfig traceConfig) {
+ spanBuilderOptions =
+ new SpanBuilderImpl.Options(randomHandler, startEndHandler, clock, traceConfig);
+ }
+
+ @Override
+ public SpanBuilder spanBuilderWithExplicitParent(String spanName, @Nullable Span parent) {
+ return SpanBuilderImpl.createWithParent(spanName, parent, spanBuilderOptions);
+ }
+
+ @Override
+ public SpanBuilder spanBuilderWithRemoteParent(
+ String spanName, @Nullable SpanContext remoteParentSpanContext) {
+ return SpanBuilderImpl.createWithRemoteParent(
+ spanName, remoteParentSpanContext, spanBuilderOptions);
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/config/TraceConfigImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/config/TraceConfigImpl.java
new file mode 100644
index 00000000..25f0c613
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/config/TraceConfigImpl.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.trace.config;
+
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+
+/**
+ * Global configuration of the trace service. This allows users to change configs for the default
+ * sampler, maximum events to be kept, etc.
+ */
+public final class TraceConfigImpl extends TraceConfig {
+ // Reads and writes are atomic for reference variables. Use volatile to ensure that these
+ // operations are visible on other CPUs as well.
+ private volatile TraceParams activeTraceParams = TraceParams.DEFAULT;
+
+ /** Constructs a new {@code TraceConfigImpl}. */
+ public TraceConfigImpl() {}
+
+ @Override
+ public TraceParams getActiveTraceParams() {
+ return activeTraceParams;
+ }
+
+ @Override
+ public void updateActiveTraceParams(TraceParams traceParams) {
+ activeTraceParams = traceParams;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/ExportComponentImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/ExportComponentImpl.java
new file mode 100644
index 00000000..19817380
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/ExportComponentImpl.java
@@ -0,0 +1,93 @@
+/*
+ * 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.trace.export;
+
+import io.opencensus.common.Duration;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SampledSpanStore;
+
+/** Implementation of the {@link ExportComponent}. */
+public final class ExportComponentImpl extends ExportComponent {
+ private static final int EXPORTER_BUFFER_SIZE = 32;
+ // Enforces that trace export exports data at least once every 5 seconds.
+ private static final Duration EXPORTER_SCHEDULE_DELAY = Duration.create(5, 0);
+
+ private final SpanExporterImpl spanExporter;
+ private final RunningSpanStoreImpl runningSpanStore;
+ private final SampledSpanStoreImpl sampledSpanStore;
+
+ @Override
+ public SpanExporterImpl getSpanExporter() {
+ return spanExporter;
+ }
+
+ @Override
+ public RunningSpanStoreImpl getRunningSpanStore() {
+ return runningSpanStore;
+ }
+
+ @Override
+ public SampledSpanStoreImpl getSampledSpanStore() {
+ return sampledSpanStore;
+ }
+
+ @Override
+ public void shutdown() {
+ sampledSpanStore.shutdown();
+ spanExporter.shutdown();
+ }
+
+ /**
+ * Returns a new {@code ExportComponentImpl} that has valid instances for {@link RunningSpanStore}
+ * and {@link SampledSpanStore}.
+ *
+ * @return a new {@code ExportComponentImpl}.
+ */
+ public static ExportComponentImpl createWithInProcessStores(EventQueue eventQueue) {
+ return new ExportComponentImpl(true, eventQueue);
+ }
+
+ /**
+ * Returns a new {@code ExportComponentImpl} that has {@code null} instances for {@link
+ * RunningSpanStore} and {@link SampledSpanStore}.
+ *
+ * @return a new {@code ExportComponentImpl}.
+ */
+ public static ExportComponentImpl createWithoutInProcessStores(EventQueue eventQueue) {
+ return new ExportComponentImpl(false, eventQueue);
+ }
+
+ /**
+ * Constructs a new {@code ExportComponentImpl}.
+ *
+ * @param supportInProcessStores {@code true} to instantiate {@link RunningSpanStore} and {@link
+ * SampledSpanStore}.
+ */
+ private ExportComponentImpl(boolean supportInProcessStores, EventQueue eventQueue) {
+ this.spanExporter = SpanExporterImpl.create(EXPORTER_BUFFER_SIZE, EXPORTER_SCHEDULE_DELAY);
+ this.runningSpanStore =
+ supportInProcessStores
+ ? new InProcessRunningSpanStoreImpl()
+ : RunningSpanStoreImpl.getNoopRunningSpanStoreImpl();
+ this.sampledSpanStore =
+ supportInProcessStores
+ ? new InProcessSampledSpanStoreImpl(eventQueue)
+ : SampledSpanStoreImpl.getNoopSampledSpanStoreImpl();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImpl.java
new file mode 100644
index 00000000..f7aeac71
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018, 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.trace.export;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.internal.ConcurrentIntrusiveList;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SpanData;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** In-process implementation of the {@link RunningSpanStore}. */
+@ThreadSafe
+public final class InProcessRunningSpanStoreImpl extends RunningSpanStoreImpl {
+ private final ConcurrentIntrusiveList<RecordEventsSpanImpl> runningSpans;
+
+ public InProcessRunningSpanStoreImpl() {
+ runningSpans = new ConcurrentIntrusiveList<RecordEventsSpanImpl>();
+ }
+
+ @Override
+ public void onStart(RecordEventsSpanImpl span) {
+ runningSpans.addElement(span);
+ }
+
+ @Override
+ public void onEnd(RecordEventsSpanImpl span) {
+ runningSpans.removeElement(span);
+ }
+
+ @Override
+ public Summary getSummary() {
+ Collection<RecordEventsSpanImpl> allRunningSpans = runningSpans.getAll();
+ Map<String, Integer> numSpansPerName = new HashMap<String, Integer>();
+ for (RecordEventsSpanImpl span : allRunningSpans) {
+ Integer prevValue = numSpansPerName.get(span.getName());
+ numSpansPerName.put(span.getName(), prevValue != null ? prevValue + 1 : 1);
+ }
+ Map<String, PerSpanNameSummary> perSpanNameSummary = new HashMap<String, PerSpanNameSummary>();
+ for (Map.Entry<String, Integer> it : numSpansPerName.entrySet()) {
+ perSpanNameSummary.put(it.getKey(), PerSpanNameSummary.create(it.getValue()));
+ }
+ Summary summary = Summary.create(perSpanNameSummary);
+ return summary;
+ }
+
+ @Override
+ public Collection<SpanData> getRunningSpans(Filter filter) {
+ Collection<RecordEventsSpanImpl> allRunningSpans = runningSpans.getAll();
+ int maxSpansToReturn =
+ filter.getMaxSpansToReturn() == 0 ? allRunningSpans.size() : filter.getMaxSpansToReturn();
+ List<SpanData> ret = new ArrayList<SpanData>(maxSpansToReturn);
+ for (RecordEventsSpanImpl span : allRunningSpans) {
+ if (ret.size() == maxSpansToReturn) {
+ break;
+ }
+ if (span.getName().equals(filter.getSpanName())) {
+ ret.add(span.toSpanData());
+ }
+ }
+ return ret;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImpl.java
new file mode 100644
index 00000000..0d8e493b
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImpl.java
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2018, 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.trace.export;
+
+import com.google.common.collect.EvictingQueue;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.export.SampledSpanStore;
+import io.opencensus.trace.export.SpanData;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** In-process implementation of the {@link SampledSpanStore}. */
+@ThreadSafe
+public final class InProcessSampledSpanStoreImpl extends SampledSpanStoreImpl {
+ private static final int NUM_SAMPLES_PER_LATENCY_BUCKET = 10;
+ private static final int NUM_SAMPLES_PER_ERROR_BUCKET = 5;
+ private static final long TIME_BETWEEN_SAMPLES = TimeUnit.SECONDS.toNanos(1);
+ private static final int NUM_LATENCY_BUCKETS = LatencyBucketBoundaries.values().length;
+ // The total number of canonical codes - 1 (the OK code).
+ private static final int NUM_ERROR_BUCKETS = CanonicalCode.values().length - 1;
+ private static final int MAX_PER_SPAN_NAME_SAMPLES =
+ NUM_SAMPLES_PER_LATENCY_BUCKET * NUM_LATENCY_BUCKETS
+ + NUM_SAMPLES_PER_ERROR_BUCKET * NUM_ERROR_BUCKETS;
+
+ // Used to stream the register/unregister events to the implementation to avoid lock contention
+ // between the main threads and the worker thread.
+ private final EventQueue eventQueue;
+
+ @GuardedBy("samples")
+ private final Map<String, PerSpanNameSamples> samples;
+
+ private static final class Bucket {
+
+ private final EvictingQueue<RecordEventsSpanImpl> sampledSpansQueue;
+ private final EvictingQueue<RecordEventsSpanImpl> notSampledSpansQueue;
+ private long lastSampledNanoTime;
+ private long lastNotSampledNanoTime;
+
+ private Bucket(int numSamples) {
+ sampledSpansQueue = EvictingQueue.create(numSamples);
+ notSampledSpansQueue = EvictingQueue.create(numSamples);
+ }
+
+ private void considerForSampling(RecordEventsSpanImpl span) {
+ long spanEndNanoTime = span.getEndNanoTime();
+ if (span.getContext().getTraceOptions().isSampled()) {
+ // Need to compare by doing the subtraction all the time because in case of an overflow,
+ // this may never sample again (at least for the next ~200 years). No real chance to
+ // overflow two times because that means the process runs for ~200 years.
+ if (spanEndNanoTime - lastSampledNanoTime > TIME_BETWEEN_SAMPLES) {
+ sampledSpansQueue.add(span);
+ lastSampledNanoTime = spanEndNanoTime;
+ }
+ } else {
+ // Need to compare by doing the subtraction all the time because in case of an overflow,
+ // this may never sample again (at least for the next ~200 years). No real chance to
+ // overflow two times because that means the process runs for ~200 years.
+ if (spanEndNanoTime - lastNotSampledNanoTime > TIME_BETWEEN_SAMPLES) {
+ notSampledSpansQueue.add(span);
+ lastNotSampledNanoTime = spanEndNanoTime;
+ }
+ }
+ }
+
+ private void getSamples(int maxSpansToReturn, List<RecordEventsSpanImpl> output) {
+ getSamples(maxSpansToReturn, output, sampledSpansQueue);
+ getSamples(maxSpansToReturn, output, notSampledSpansQueue);
+ }
+
+ private static void getSamples(
+ int maxSpansToReturn,
+ List<RecordEventsSpanImpl> output,
+ EvictingQueue<RecordEventsSpanImpl> queue) {
+ for (RecordEventsSpanImpl span : queue) {
+ if (output.size() >= maxSpansToReturn) {
+ break;
+ }
+ output.add(span);
+ }
+ }
+
+ private void getSamplesFilteredByLatency(
+ long latencyLowerNs,
+ long latencyUpperNs,
+ int maxSpansToReturn,
+ List<RecordEventsSpanImpl> output) {
+ getSamplesFilteredByLatency(
+ latencyLowerNs, latencyUpperNs, maxSpansToReturn, output, sampledSpansQueue);
+ getSamplesFilteredByLatency(
+ latencyLowerNs, latencyUpperNs, maxSpansToReturn, output, notSampledSpansQueue);
+ }
+
+ private static void getSamplesFilteredByLatency(
+ long latencyLowerNs,
+ long latencyUpperNs,
+ int maxSpansToReturn,
+ List<RecordEventsSpanImpl> output,
+ EvictingQueue<RecordEventsSpanImpl> queue) {
+ for (RecordEventsSpanImpl span : queue) {
+ if (output.size() >= maxSpansToReturn) {
+ break;
+ }
+ long spanLatencyNs = span.getLatencyNs();
+ if (spanLatencyNs >= latencyLowerNs && spanLatencyNs < latencyUpperNs) {
+ output.add(span);
+ }
+ }
+ }
+
+ private int getNumSamples() {
+ return sampledSpansQueue.size() + notSampledSpansQueue.size();
+ }
+ }
+
+ /**
+ * Keeps samples for a given span name. Samples for all the latency buckets and for all canonical
+ * codes other than OK.
+ */
+ private static final class PerSpanNameSamples {
+
+ private final Bucket[] latencyBuckets;
+ private final Bucket[] errorBuckets;
+
+ private PerSpanNameSamples() {
+ latencyBuckets = new Bucket[NUM_LATENCY_BUCKETS];
+ for (int i = 0; i < NUM_LATENCY_BUCKETS; i++) {
+ latencyBuckets[i] = new Bucket(NUM_SAMPLES_PER_LATENCY_BUCKET);
+ }
+ errorBuckets = new Bucket[NUM_ERROR_BUCKETS];
+ for (int i = 0; i < NUM_ERROR_BUCKETS; i++) {
+ errorBuckets[i] = new Bucket(NUM_SAMPLES_PER_ERROR_BUCKET);
+ }
+ }
+
+ @Nullable
+ private Bucket getLatencyBucket(long latencyNs) {
+ for (int i = 0; i < NUM_LATENCY_BUCKETS; i++) {
+ LatencyBucketBoundaries boundaries = LatencyBucketBoundaries.values()[i];
+ if (latencyNs >= boundaries.getLatencyLowerNs()
+ && latencyNs < boundaries.getLatencyUpperNs()) {
+ return latencyBuckets[i];
+ }
+ }
+ // latencyNs is negative or Long.MAX_VALUE, so this Span can be ignored. This cannot happen
+ // in real production because System#nanoTime is monotonic.
+ return null;
+ }
+
+ private Bucket getErrorBucket(CanonicalCode code) {
+ return errorBuckets[code.value() - 1];
+ }
+
+ private void considerForSampling(RecordEventsSpanImpl span) {
+ Status status = span.getStatus();
+ // Null status means running Span, this should not happen in production, but the library
+ // should not crash because of this.
+ if (status != null) {
+ Bucket bucket =
+ status.isOk()
+ ? getLatencyBucket(span.getLatencyNs())
+ : getErrorBucket(status.getCanonicalCode());
+ // If unable to find the bucket, ignore this Span.
+ if (bucket != null) {
+ bucket.considerForSampling(span);
+ }
+ }
+ }
+
+ private Map<LatencyBucketBoundaries, Integer> getNumbersOfLatencySampledSpans() {
+ Map<LatencyBucketBoundaries, Integer> latencyBucketSummaries =
+ new EnumMap<LatencyBucketBoundaries, Integer>(LatencyBucketBoundaries.class);
+ for (int i = 0; i < NUM_LATENCY_BUCKETS; i++) {
+ latencyBucketSummaries.put(
+ LatencyBucketBoundaries.values()[i], latencyBuckets[i].getNumSamples());
+ }
+ return latencyBucketSummaries;
+ }
+
+ private Map<CanonicalCode, Integer> getNumbersOfErrorSampledSpans() {
+ Map<CanonicalCode, Integer> errorBucketSummaries =
+ new EnumMap<CanonicalCode, Integer>(CanonicalCode.class);
+ for (int i = 0; i < NUM_ERROR_BUCKETS; i++) {
+ errorBucketSummaries.put(CanonicalCode.values()[i + 1], errorBuckets[i].getNumSamples());
+ }
+ return errorBucketSummaries;
+ }
+
+ private List<RecordEventsSpanImpl> getErrorSamples(
+ @Nullable CanonicalCode code, int maxSpansToReturn) {
+ ArrayList<RecordEventsSpanImpl> output =
+ new ArrayList<RecordEventsSpanImpl>(maxSpansToReturn);
+ if (code != null) {
+ getErrorBucket(code).getSamples(maxSpansToReturn, output);
+ } else {
+ for (int i = 0; i < NUM_ERROR_BUCKETS; i++) {
+ errorBuckets[i].getSamples(maxSpansToReturn, output);
+ }
+ }
+ return output;
+ }
+
+ private List<RecordEventsSpanImpl> getLatencySamples(
+ long latencyLowerNs, long latencyUpperNs, int maxSpansToReturn) {
+ ArrayList<RecordEventsSpanImpl> output =
+ new ArrayList<RecordEventsSpanImpl>(maxSpansToReturn);
+ for (int i = 0; i < NUM_LATENCY_BUCKETS; i++) {
+ LatencyBucketBoundaries boundaries = LatencyBucketBoundaries.values()[i];
+ if (latencyUpperNs >= boundaries.getLatencyLowerNs()
+ && latencyLowerNs < boundaries.getLatencyUpperNs()) {
+ latencyBuckets[i].getSamplesFilteredByLatency(
+ latencyLowerNs, latencyUpperNs, maxSpansToReturn, output);
+ }
+ }
+ return output;
+ }
+ }
+
+ /** Constructs a new {@code InProcessSampledSpanStoreImpl}. */
+ InProcessSampledSpanStoreImpl(EventQueue eventQueue) {
+ samples = new HashMap<String, PerSpanNameSamples>();
+ this.eventQueue = eventQueue;
+ }
+
+ @Override
+ public Summary getSummary() {
+ Map<String, PerSpanNameSummary> ret = new HashMap<String, PerSpanNameSummary>();
+ synchronized (samples) {
+ for (Map.Entry<String, PerSpanNameSamples> it : samples.entrySet()) {
+ ret.put(
+ it.getKey(),
+ PerSpanNameSummary.create(
+ it.getValue().getNumbersOfLatencySampledSpans(),
+ it.getValue().getNumbersOfErrorSampledSpans()));
+ }
+ }
+ return Summary.create(ret);
+ }
+
+ @Override
+ public void considerForSampling(RecordEventsSpanImpl span) {
+ synchronized (samples) {
+ String spanName = span.getName();
+ if (span.getSampleToLocalSpanStore() && !samples.containsKey(spanName)) {
+ samples.put(spanName, new PerSpanNameSamples());
+ }
+ PerSpanNameSamples perSpanNameSamples = samples.get(spanName);
+ if (perSpanNameSamples != null) {
+ perSpanNameSamples.considerForSampling(span);
+ }
+ }
+ }
+
+ @Override
+ public void registerSpanNamesForCollection(Collection<String> spanNames) {
+ eventQueue.enqueue(new RegisterSpanNameEvent(this, spanNames));
+ }
+
+ @Override
+ protected void shutdown() {
+ eventQueue.shutdown();
+ }
+
+ private void internaltRegisterSpanNamesForCollection(Collection<String> spanNames) {
+ synchronized (samples) {
+ for (String spanName : spanNames) {
+ if (!samples.containsKey(spanName)) {
+ samples.put(spanName, new PerSpanNameSamples());
+ }
+ }
+ }
+ }
+
+ private static final class RegisterSpanNameEvent implements EventQueue.Entry {
+ private final InProcessSampledSpanStoreImpl sampledSpanStore;
+ private final Collection<String> spanNames;
+
+ private RegisterSpanNameEvent(
+ InProcessSampledSpanStoreImpl sampledSpanStore, Collection<String> spanNames) {
+ this.sampledSpanStore = sampledSpanStore;
+ this.spanNames = new ArrayList<String>(spanNames);
+ }
+
+ @Override
+ public void process() {
+ sampledSpanStore.internaltRegisterSpanNamesForCollection(spanNames);
+ }
+ }
+
+ @Override
+ public void unregisterSpanNamesForCollection(Collection<String> spanNames) {
+ eventQueue.enqueue(new UnregisterSpanNameEvent(this, spanNames));
+ }
+
+ private void internalUnregisterSpanNamesForCollection(Collection<String> spanNames) {
+ synchronized (samples) {
+ samples.keySet().removeAll(spanNames);
+ }
+ }
+
+ private static final class UnregisterSpanNameEvent implements EventQueue.Entry {
+ private final InProcessSampledSpanStoreImpl sampledSpanStore;
+ private final Collection<String> spanNames;
+
+ private UnregisterSpanNameEvent(
+ InProcessSampledSpanStoreImpl sampledSpanStore, Collection<String> spanNames) {
+ this.sampledSpanStore = sampledSpanStore;
+ this.spanNames = new ArrayList<String>(spanNames);
+ }
+
+ @Override
+ public void process() {
+ sampledSpanStore.internalUnregisterSpanNamesForCollection(spanNames);
+ }
+ }
+
+ @Override
+ public Set<String> getRegisteredSpanNamesForCollection() {
+ synchronized (samples) {
+ return Collections.unmodifiableSet(new HashSet<String>(samples.keySet()));
+ }
+ }
+
+ @Override
+ public Collection<SpanData> getErrorSampledSpans(ErrorFilter filter) {
+ int numSpansToReturn =
+ filter.getMaxSpansToReturn() == 0
+ ? MAX_PER_SPAN_NAME_SAMPLES
+ : filter.getMaxSpansToReturn();
+ List<RecordEventsSpanImpl> spans = Collections.emptyList();
+ // Try to not keep the lock to much, do the RecordEventsSpanImpl -> SpanData conversion outside
+ // the lock.
+ synchronized (samples) {
+ PerSpanNameSamples perSpanNameSamples = samples.get(filter.getSpanName());
+ if (perSpanNameSamples != null) {
+ spans = perSpanNameSamples.getErrorSamples(filter.getCanonicalCode(), numSpansToReturn);
+ }
+ }
+ List<SpanData> ret = new ArrayList<SpanData>(spans.size());
+ for (RecordEventsSpanImpl span : spans) {
+ ret.add(span.toSpanData());
+ }
+ return Collections.unmodifiableList(ret);
+ }
+
+ @Override
+ public Collection<SpanData> getLatencySampledSpans(LatencyFilter filter) {
+ int numSpansToReturn =
+ filter.getMaxSpansToReturn() == 0
+ ? MAX_PER_SPAN_NAME_SAMPLES
+ : filter.getMaxSpansToReturn();
+ List<RecordEventsSpanImpl> spans = Collections.emptyList();
+ // Try to not keep the lock to much, do the RecordEventsSpanImpl -> SpanData conversion outside
+ // the lock.
+ synchronized (samples) {
+ PerSpanNameSamples perSpanNameSamples = samples.get(filter.getSpanName());
+ if (perSpanNameSamples != null) {
+ spans =
+ perSpanNameSamples.getLatencySamples(
+ filter.getLatencyLowerNs(), filter.getLatencyUpperNs(), numSpansToReturn);
+ }
+ }
+ List<SpanData> ret = new ArrayList<SpanData>(spans.size());
+ for (RecordEventsSpanImpl span : spans) {
+ ret.add(span.toSpanData());
+ }
+ return Collections.unmodifiableList(ret);
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/RunningSpanStoreImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/RunningSpanStoreImpl.java
new file mode 100644
index 00000000..962f5b01
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/RunningSpanStoreImpl.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018, 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.trace.export;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.trace.export.RunningSpanStore;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+
+/** Abstract implementation of the {@link RunningSpanStore}. */
+public abstract class RunningSpanStoreImpl extends RunningSpanStore {
+
+ private static final RunningSpanStoreImpl NOOP_RUNNING_SPAN_STORE_IMPL =
+ new NoopRunningSpanStoreImpl();
+
+ /** Returns the no-op implementation of the {@link RunningSpanStoreImpl}. */
+ static RunningSpanStoreImpl getNoopRunningSpanStoreImpl() {
+ return NOOP_RUNNING_SPAN_STORE_IMPL;
+ }
+
+ /**
+ * Adds the {@code Span} into the running spans list when the {@code Span} starts.
+ *
+ * @param span the {@code Span} that started.
+ */
+ public abstract void onStart(RecordEventsSpanImpl span);
+
+ /**
+ * Removes the {@code Span} from the running spans list when the {@code Span} ends.
+ *
+ * @param span the {@code Span} that ended.
+ */
+ public abstract void onEnd(RecordEventsSpanImpl span);
+
+ private static final class NoopRunningSpanStoreImpl extends RunningSpanStoreImpl {
+
+ private static final Summary EMPTY_SUMMARY =
+ RunningSpanStore.Summary.create(Collections.<String, PerSpanNameSummary>emptyMap());
+
+ @Override
+ public void onStart(RecordEventsSpanImpl span) {}
+
+ @Override
+ public void onEnd(RecordEventsSpanImpl span) {}
+
+ @Override
+ public Summary getSummary() {
+ return EMPTY_SUMMARY;
+ }
+
+ @Override
+ public Collection<SpanData> getRunningSpans(Filter filter) {
+ return Collections.<SpanData>emptyList();
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/SampledSpanStoreImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/SampledSpanStoreImpl.java
new file mode 100644
index 00000000..e67c2f8e
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/SampledSpanStoreImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2018, 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.trace.export;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.trace.export.SampledSpanStore;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/** Abstract implementation of the {@link SampledSpanStore}. */
+public abstract class SampledSpanStoreImpl extends SampledSpanStore {
+ private static final SampledSpanStoreImpl NOOP_SAMPLED_SPAN_STORE_IMPL =
+ new NoopSampledSpanStoreImpl();
+
+ /** Returns the new no-op implmentation of {@link SampledSpanStoreImpl}. */
+ public static SampledSpanStoreImpl getNoopSampledSpanStoreImpl() {
+ return NOOP_SAMPLED_SPAN_STORE_IMPL;
+ }
+
+ /**
+ * Considers to save the given spans to the stored samples. This must be called at the end of each
+ * Span with the option RECORD_EVENTS.
+ *
+ * @param span the span to be consider for storing into the store buckets.
+ */
+ public abstract void considerForSampling(RecordEventsSpanImpl span);
+
+ protected void shutdown() {}
+
+ private static final class NoopSampledSpanStoreImpl extends SampledSpanStoreImpl {
+ private static final Summary EMPTY_SUMMARY =
+ Summary.create(Collections.<String, PerSpanNameSummary>emptyMap());
+ private static final Set<String> EMPTY_REGISTERED_SPAN_NAMES = Collections.<String>emptySet();
+ private static final Collection<SpanData> EMPTY_SPANDATA = Collections.<SpanData>emptySet();
+
+ @Override
+ public Summary getSummary() {
+ return EMPTY_SUMMARY;
+ }
+
+ @Override
+ public void considerForSampling(RecordEventsSpanImpl span) {}
+
+ @Override
+ public void registerSpanNamesForCollection(Collection<String> spanNames) {}
+
+ @Override
+ public void unregisterSpanNamesForCollection(Collection<String> spanNames) {}
+
+ @Override
+ public Set<String> getRegisteredSpanNamesForCollection() {
+ return EMPTY_REGISTERED_SPAN_NAMES;
+ }
+
+ @Override
+ public Collection<SpanData> getErrorSampledSpans(ErrorFilter filter) {
+ return EMPTY_SPANDATA;
+ }
+
+ @Override
+ public Collection<SpanData> getLatencySampledSpans(LatencyFilter filter) {
+ return EMPTY_SPANDATA;
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/export/SpanExporterImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/export/SpanExporterImpl.java
new file mode 100644
index 00000000..51a7b05c
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/export/SpanExporterImpl.java
@@ -0,0 +1,214 @@
+/*
+ * 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.trace.export;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.common.Duration;
+import io.opencensus.implcore.internal.DaemonThreadFactory;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.annotation.concurrent.GuardedBy;
+
+/** Implementation of the {@link SpanExporter}. */
+public final class SpanExporterImpl extends SpanExporter {
+ private static final Logger logger = Logger.getLogger(ExportComponent.class.getName());
+
+ private final Worker worker;
+ private final Thread workerThread;
+
+ /**
+ * Constructs a {@code SpanExporterImpl} that exports the {@link SpanData} asynchronously.
+ *
+ * <p>Starts a separate thread that wakes up every {@code scheduleDelay} and exports any available
+ * spans data. If the number of buffered SpanData objects is greater than {@code bufferSize} then
+ * the thread wakes up sooner.
+ *
+ * @param bufferSize the size of the buffered span data.
+ * @param scheduleDelay the maximum delay.
+ */
+ static SpanExporterImpl create(int bufferSize, Duration scheduleDelay) {
+ // TODO(bdrutu): Consider to add a shutdown hook to not avoid dropping data.
+ Worker worker = new Worker(bufferSize, scheduleDelay);
+ return new SpanExporterImpl(worker);
+ }
+
+ /**
+ * Adds a Span to the exporting service.
+ *
+ * @param span the {@code Span} to be added.
+ */
+ public void addSpan(RecordEventsSpanImpl span) {
+ worker.addSpan(span);
+ }
+
+ @Override
+ public void registerHandler(String name, Handler handler) {
+ worker.registerHandler(name, handler);
+ }
+
+ @Override
+ public void unregisterHandler(String name) {
+ worker.unregisterHandler(name);
+ }
+
+ void flush() {
+ worker.flush();
+ }
+
+ void shutdown() {
+ flush();
+ workerThread.interrupt();
+ }
+
+ private SpanExporterImpl(Worker worker) {
+ this.workerThread =
+ new DaemonThreadFactory("ExportComponent.ServiceExporterThread").newThread(worker);
+ this.workerThread.start();
+ this.worker = worker;
+ }
+
+ @VisibleForTesting
+ Thread getServiceExporterThread() {
+ return workerThread;
+ }
+
+ // Worker in a thread that batches multiple span data and calls the registered services to export
+ // that data.
+ //
+ // The map of registered handlers is implemented using ConcurrentHashMap ensuring full
+ // concurrency of retrievals and adjustable expected concurrency for updates. Retrievals
+ // reflect the results of the most recently completed update operations held upon their onset.
+ //
+ // The list of batched data is protected by an explicit monitor object which ensures full
+ // concurrency.
+ private static final class Worker implements Runnable {
+ private final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ private final List<RecordEventsSpanImpl> spans;
+
+ private final Map<String, Handler> serviceHandlers = new ConcurrentHashMap<String, Handler>();
+ private final int bufferSize;
+ private final long scheduleDelayMillis;
+
+ // See SpanExporterImpl#addSpan.
+ private void addSpan(RecordEventsSpanImpl span) {
+ synchronized (monitor) {
+ this.spans.add(span);
+ if (spans.size() > bufferSize) {
+ monitor.notifyAll();
+ }
+ }
+ }
+
+ // See SpanExporter#registerHandler.
+ private void registerHandler(String name, Handler serviceHandler) {
+ serviceHandlers.put(name, serviceHandler);
+ }
+
+ // See SpanExporter#unregisterHandler.
+ private void unregisterHandler(String name) {
+ serviceHandlers.remove(name);
+ }
+
+ // Exports the list of SpanData to all the ServiceHandlers.
+ private void onBatchExport(List<SpanData> spanDataList) {
+ // From the java documentation of the ConcurrentHashMap#entrySet():
+ // The view's iterator is a "weakly consistent" iterator that will never throw
+ // ConcurrentModificationException, and guarantees to traverse elements as they existed
+ // upon construction of the iterator, and may (but is not guaranteed to) reflect any
+ // modifications subsequent to construction.
+ for (Map.Entry<String, Handler> it : serviceHandlers.entrySet()) {
+ // In case of any exception thrown by the service handlers continue to run.
+ try {
+ it.getValue().export(spanDataList);
+ } catch (Throwable e) {
+ logger.log(Level.WARNING, "Exception thrown by the service export " + it.getKey(), e);
+ }
+ }
+ }
+
+ private Worker(int bufferSize, Duration scheduleDelay) {
+ spans = new ArrayList<RecordEventsSpanImpl>(bufferSize);
+ this.bufferSize = bufferSize;
+ this.scheduleDelayMillis = scheduleDelay.toMillis();
+ }
+
+ // Returns an unmodifiable list of all buffered spans data to ensure that any registered
+ // service handler cannot modify the list.
+ private static List<SpanData> fromSpanImplToSpanData(List<RecordEventsSpanImpl> spans) {
+ List<SpanData> spanDatas = new ArrayList<SpanData>(spans.size());
+ for (RecordEventsSpanImpl span : spans) {
+ spanDatas.add(span.toSpanData());
+ }
+ return Collections.unmodifiableList(spanDatas);
+ }
+
+ @Override
+ public void run() {
+ while (true) {
+ // Copy all the batched spans in a separate list to release the monitor lock asap to
+ // avoid blocking the producer thread.
+ List<RecordEventsSpanImpl> spansCopy;
+ synchronized (monitor) {
+ if (spans.size() < bufferSize) {
+ do {
+ // In the case of a spurious wakeup we export only if we have at least one span in
+ // the batch. It is acceptable because batching is a best effort mechanism here.
+ try {
+ monitor.wait(scheduleDelayMillis);
+ } catch (InterruptedException ie) {
+ // Preserve the interruption status as per guidance and stop doing any work.
+ Thread.currentThread().interrupt();
+ return;
+ }
+ } while (spans.isEmpty());
+ }
+ spansCopy = new ArrayList<RecordEventsSpanImpl>(spans);
+ spans.clear();
+ }
+ // Execute the batch export outside the synchronized to not block all producers.
+ final List<SpanData> spanDataList = fromSpanImplToSpanData(spansCopy);
+ if (!spanDataList.isEmpty()) {
+ onBatchExport(spanDataList);
+ }
+ }
+ }
+
+ void flush() {
+ List<RecordEventsSpanImpl> spansCopy;
+ synchronized (monitor) {
+ spansCopy = new ArrayList<RecordEventsSpanImpl>(spans);
+ spans.clear();
+ }
+
+ final List<SpanData> spanDataList = fromSpanImplToSpanData(spansCopy);
+ if (!spanDataList.isEmpty()) {
+ onBatchExport(spanDataList);
+ }
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveList.java b/impl_core/src/main/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveList.java
new file mode 100644
index 00000000..22d8e41a
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveList.java
@@ -0,0 +1,181 @@
+/*
+ * 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.trace.internal;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import io.opencensus.implcore.internal.CheckerFrameworkUtils;
+import io.opencensus.implcore.trace.internal.ConcurrentIntrusiveList.Element;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An {@code ConcurrentIntrusiveList<T>} is a doubly-linked list where the link pointers are
+ * embedded in the elements. This makes insertion and removal into a known position constant time.
+ *
+ * <p>Elements must derive from the {@code Element<T extends Element<T>>} interface:
+ *
+ * <pre><code>
+ * class MyClass implements {@code Element<MyClass>} {
+ * private MyClass next = null;
+ * private MyClass prev = null;
+ *
+ * {@literal @}Override
+ * MyClass getNext() {
+ * return next;
+ * }
+ *
+ * {@literal @}Override
+ * void setNext(MyClass element) {
+ * next = element;
+ * }
+ *
+ * {@literal @}Override
+ * MyClass getPrev() {
+ * return prev;
+ * }
+ *
+ * {@literal @}Override
+ * void setPrev(MyClass element) {
+ * prev = element;
+ * }
+ * }
+ * </code></pre>
+ */
+@ThreadSafe
+public final class ConcurrentIntrusiveList<T extends Element<T>> {
+ private int size = 0;
+ @Nullable private T head = null;
+
+ public ConcurrentIntrusiveList() {}
+
+ /**
+ * Adds the given {@code element} to the list.
+ *
+ * @param element the element to add.
+ * @throws IllegalArgumentException if the element is already in a list.
+ */
+ public synchronized void addElement(T element) {
+ checkArgument(
+ element.getNext() == null && element.getPrev() == null && element != head,
+ "Element already in a list.");
+ size++;
+ if (head == null) {
+ head = element;
+ } else {
+ head.setPrev(element);
+ element.setNext(head);
+ head = element;
+ }
+ }
+
+ /**
+ * Removes the given {@code element} from the list.
+ *
+ * @param element the element to remove.
+ * @throws IllegalArgumentException if the element is not in the list.
+ */
+ public synchronized void removeElement(T element) {
+ checkArgument(
+ element.getNext() != null || element.getPrev() != null || element == head,
+ "Element not in the list.");
+ size--;
+ if (element.getPrev() == null) {
+ // This is the first element
+ head = element.getNext();
+ if (head != null) {
+ // If more than one element in the list.
+ head.setPrev(null);
+ element.setNext(null);
+ }
+ } else if (element.getNext() == null) {
+ // This is the last element, and there is at least another element because
+ // element.getPrev() != null.
+ CheckerFrameworkUtils.castNonNull(element.getPrev()).setNext(null);
+ element.setPrev(null);
+ } else {
+ CheckerFrameworkUtils.castNonNull(element.getPrev()).setNext(element.getNext());
+ CheckerFrameworkUtils.castNonNull(element.getNext()).setPrev(element.getPrev());
+ element.setNext(null);
+ element.setPrev(null);
+ }
+ }
+
+ /**
+ * Returns the number of elements in this list.
+ *
+ * @return the number of elements in this list.
+ */
+ public synchronized int size() {
+ return size;
+ }
+
+ /**
+ * Returns all the elements from this list.
+ *
+ * @return all the elements from this list.
+ */
+ public synchronized Collection<T> getAll() {
+ List<T> all = new ArrayList<T>(size);
+ for (T e = head; e != null; e = e.getNext()) {
+ all.add(e);
+ }
+ return all;
+ }
+
+ /**
+ * This is an interface that must be implemented by any element that uses {@link
+ * ConcurrentIntrusiveList}.
+ *
+ * @param <T> the element that will be used for the list.
+ */
+ public interface Element<T extends Element<T>> {
+
+ /**
+ * Returns a reference to the next element in the list.
+ *
+ * @return a reference to the next element in the list.
+ */
+ @Nullable
+ T getNext();
+
+ /**
+ * Sets the reference to the next element in the list.
+ *
+ * @param element the reference to the next element in the list.
+ */
+ void setNext(@Nullable T element);
+
+ /**
+ * Returns a reference to the previous element in the list.
+ *
+ * @return a reference to the previous element in the list.
+ */
+ @Nullable
+ T getPrev();
+
+ /**
+ * Sets the reference to the previous element in the list.
+ *
+ * @param element the reference to the previous element in the list.
+ */
+ void setPrev(@Nullable T element);
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/internal/RandomHandler.java b/impl_core/src/main/java/io/opencensus/implcore/trace/internal/RandomHandler.java
new file mode 100644
index 00000000..70be5a90
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/internal/RandomHandler.java
@@ -0,0 +1,50 @@
+/*
+ * 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.trace.internal;
+
+import java.security.SecureRandom;
+import java.util.Random;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Abstract class to access the current {@link Random}.
+ *
+ * <p>Implementation can have a per thread instance or a single global instance.
+ */
+@ThreadSafe
+public abstract class RandomHandler {
+ /**
+ * Returns the current {@link Random}.
+ *
+ * @return the current {@code Random}.
+ */
+ public abstract Random current();
+
+ /** Implementation of the {@link RandomHandler} using {@link SecureRandom}. */
+ @ThreadSafe
+ public static final class SecureRandomHandler extends RandomHandler {
+ private final Random random = new SecureRandom();
+
+ /** Constructs a new {@link SecureRandomHandler}. */
+ public SecureRandomHandler() {}
+
+ @Override
+ public Random current() {
+ return random;
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/B3Format.java b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/B3Format.java
new file mode 100644
index 00000000..d928d93c
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/B3Format.java
@@ -0,0 +1,113 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/*>>>
+import org.checkerframework.checker.nullness.qual.NonNull;
+*/
+
+/**
+ * Implementation of the B3 propagation protocol. See <a
+ * href=https://github.com/openzipkin/b3-propagation>b3-propagation</a>.
+ */
+final class B3Format extends TextFormat {
+ private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+ @VisibleForTesting static final String X_B3_TRACE_ID = "X-B3-TraceId";
+ @VisibleForTesting static final String X_B3_SPAN_ID = "X-B3-SpanId";
+ @VisibleForTesting static final String X_B3_PARENT_SPAN_ID = "X-B3-ParentSpanId";
+ @VisibleForTesting static final String X_B3_SAMPLED = "X-B3-Sampled";
+ @VisibleForTesting static final String X_B3_FLAGS = "X-B3-Flags";
+ private static final List<String> FIELDS =
+ Collections.unmodifiableList(
+ Arrays.asList(
+ X_B3_TRACE_ID, X_B3_SPAN_ID, X_B3_PARENT_SPAN_ID, X_B3_SAMPLED, X_B3_FLAGS));
+
+ // Used as the upper TraceId.SIZE hex characters of the traceID. B3-propagation used to send
+ // TraceId.SIZE hex characters (8-bytes traceId) in the past.
+ private static final String UPPER_TRACE_ID = "0000000000000000";
+ // Sampled value via the X_B3_SAMPLED header.
+ private static final String SAMPLED_VALUE = "1";
+ // "Debug" sampled value.
+ private static final String FLAGS_VALUE = "1";
+
+ @Override
+ public List<String> fields() {
+ return FIELDS;
+ }
+
+ @Override
+ public <C /*>>> extends @NonNull Object*/> void inject(
+ SpanContext spanContext, C carrier, Setter<C> setter) {
+ checkNotNull(spanContext, "spanContext");
+ checkNotNull(setter, "setter");
+ checkNotNull(carrier, "carrier");
+ setter.put(carrier, X_B3_TRACE_ID, spanContext.getTraceId().toLowerBase16());
+ setter.put(carrier, X_B3_SPAN_ID, spanContext.getSpanId().toLowerBase16());
+ if (spanContext.getTraceOptions().isSampled()) {
+ setter.put(carrier, X_B3_SAMPLED, SAMPLED_VALUE);
+ }
+ }
+
+ @Override
+ public <C /*>>> extends @NonNull Object*/> SpanContext extract(C carrier, Getter<C> getter)
+ throws SpanContextParseException {
+ checkNotNull(carrier, "carrier");
+ checkNotNull(getter, "getter");
+ try {
+ TraceId traceId;
+ String traceIdStr = getter.get(carrier, X_B3_TRACE_ID);
+ if (traceIdStr != null) {
+ if (traceIdStr.length() == TraceId.SIZE) {
+ // This is an 8-byte traceID.
+ traceIdStr = UPPER_TRACE_ID + traceIdStr;
+ }
+ traceId = TraceId.fromLowerBase16(traceIdStr);
+ } else {
+ throw new SpanContextParseException("Missing X_B3_TRACE_ID.");
+ }
+ SpanId spanId;
+ String spanIdStr = getter.get(carrier, X_B3_SPAN_ID);
+ if (spanIdStr != null) {
+ spanId = SpanId.fromLowerBase16(spanIdStr);
+ } else {
+ throw new SpanContextParseException("Missing X_B3_SPAN_ID.");
+ }
+ TraceOptions traceOptions = TraceOptions.DEFAULT;
+ if (SAMPLED_VALUE.equals(getter.get(carrier, X_B3_SAMPLED))
+ || FLAGS_VALUE.equals(getter.get(carrier, X_B3_FLAGS))) {
+ traceOptions = TraceOptions.builder().setIsSampled(true).build();
+ }
+ return SpanContext.create(traceId, spanId, traceOptions, TRACESTATE_DEFAULT);
+ } catch (IllegalArgumentException e) {
+ throw new SpanContextParseException("Invalid input.", e);
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/BinaryFormatImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/BinaryFormatImpl.java
new file mode 100644
index 00000000..233fbd31
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/BinaryFormatImpl.java
@@ -0,0 +1,148 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import io.opencensus.trace.propagation.BinaryFormat;
+import io.opencensus.trace.propagation.SpanContextParseException;
+
+/**
+ * Implementation of the {@link BinaryFormat}.
+ *
+ * <p>BinaryFormat format:
+ *
+ * <ul>
+ * <li>Binary value: &lt;version_id&gt;&lt;version_format&gt;
+ * <li>version_id: 1-byte representing the version id.
+ * <li>For version_id = 0:
+ * <ul>
+ * <li>version_format: &lt;field&gt;&lt;field&gt;
+ * <li>field_format: &lt;field_id&gt;&lt;field_format&gt;
+ * <li>Fields:
+ * <ul>
+ * <li>TraceId: (field_id = 0, len = 16, default = &#34;0000000000000000&#34;) -
+ * 16-byte array representing the trace_id.
+ * <li>SpanId: (field_id = 1, len = 8, default = &#34;00000000&#34;) - 8-byte array
+ * representing the span_id.
+ * <li>TraceOptions: (field_id = 2, len = 1, default = &#34;0&#34;) - 1-byte array
+ * representing the trace_options.
+ * </ul>
+ * <li>Fields MUST be encoded using the field id order (smaller to higher).
+ * <li>Valid value example:
+ * <ul>
+ * <li>{0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97,
+ * 98, 99, 100, 101, 102, 103, 104, 2, 1}
+ * <li>version_id = 0;
+ * <li>trace_id = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79}
+ * <li>span_id = {97, 98, 99, 100, 101, 102, 103, 104};
+ * <li>trace_options = {1};
+ * </ul>
+ * </ul>
+ * </ul>
+ */
+final class BinaryFormatImpl extends BinaryFormat {
+ private static final Tracestate TRACESTATE_DEFAULT = Tracestate.builder().build();
+ private static final byte VERSION_ID = 0;
+ private static final int VERSION_ID_OFFSET = 0;
+ // The version_id/field_id size in bytes.
+ private static final byte ID_SIZE = 1;
+ private static final byte TRACE_ID_FIELD_ID = 0;
+
+ // TODO: clarify if offsets are correct here. While the specification suggests you should stop
+ // parsing when you hit an unknown field, it does not suggest that fields must be declared in
+ // ID order. Rather it only groups by data type order, in this case Trace Context
+ // https://github.com/census-instrumentation/opencensus-specs/blob/master/encodings/BinaryEncoding.md#deserialization-rules
+ @VisibleForTesting static final int TRACE_ID_FIELD_ID_OFFSET = VERSION_ID_OFFSET + ID_SIZE;
+
+ private static final int TRACE_ID_OFFSET = TRACE_ID_FIELD_ID_OFFSET + ID_SIZE;
+ private static final byte SPAN_ID_FIELD_ID = 1;
+
+ @VisibleForTesting static final int SPAN_ID_FIELD_ID_OFFSET = TRACE_ID_OFFSET + TraceId.SIZE;
+
+ private static final int SPAN_ID_OFFSET = SPAN_ID_FIELD_ID_OFFSET + ID_SIZE;
+ private static final byte TRACE_OPTION_FIELD_ID = 2;
+
+ @VisibleForTesting static final int TRACE_OPTION_FIELD_ID_OFFSET = SPAN_ID_OFFSET + SpanId.SIZE;
+
+ private static final int TRACE_OPTIONS_OFFSET = TRACE_OPTION_FIELD_ID_OFFSET + ID_SIZE;
+ /** Version, Trace and Span IDs are required fields. */
+ private static final int REQUIRED_FORMAT_LENGTH = 3 * ID_SIZE + TraceId.SIZE + SpanId.SIZE;
+ /** Use {@link TraceOptions#DEFAULT} unless its optional field is present. */
+ private static final int ALL_FORMAT_LENGTH = REQUIRED_FORMAT_LENGTH + ID_SIZE + TraceOptions.SIZE;
+
+ @Override
+ public byte[] toByteArray(SpanContext spanContext) {
+ checkNotNull(spanContext, "spanContext");
+ byte[] bytes = new byte[ALL_FORMAT_LENGTH];
+ bytes[VERSION_ID_OFFSET] = VERSION_ID;
+ bytes[TRACE_ID_FIELD_ID_OFFSET] = TRACE_ID_FIELD_ID;
+ spanContext.getTraceId().copyBytesTo(bytes, TRACE_ID_OFFSET);
+ bytes[SPAN_ID_FIELD_ID_OFFSET] = SPAN_ID_FIELD_ID;
+ spanContext.getSpanId().copyBytesTo(bytes, SPAN_ID_OFFSET);
+ bytes[TRACE_OPTION_FIELD_ID_OFFSET] = TRACE_OPTION_FIELD_ID;
+ spanContext.getTraceOptions().copyBytesTo(bytes, TRACE_OPTIONS_OFFSET);
+ return bytes;
+ }
+
+ @Override
+ public SpanContext fromByteArray(byte[] bytes) throws SpanContextParseException {
+ checkNotNull(bytes, "bytes");
+ if (bytes.length == 0 || bytes[0] != VERSION_ID) {
+ throw new SpanContextParseException("Unsupported version.");
+ }
+ if (bytes.length < REQUIRED_FORMAT_LENGTH) {
+ throw new SpanContextParseException("Invalid input: truncated");
+ }
+ // TODO: the following logic assumes that fields are written in ID order. The spec does not say
+ // that. If it decides not to, this logic would need to be more like a loop
+ TraceId traceId;
+ SpanId spanId;
+ TraceOptions traceOptions = TraceOptions.DEFAULT;
+ int pos = 1;
+ if (bytes[pos] == TRACE_ID_FIELD_ID) {
+ traceId = TraceId.fromBytes(bytes, pos + ID_SIZE);
+ pos += ID_SIZE + TraceId.SIZE;
+ } else {
+ // TODO: update the spec to suggest that the trace ID is not actually optional
+ throw new SpanContextParseException("Invalid input: expected trace ID at offset " + pos);
+ }
+ if (bytes[pos] == SPAN_ID_FIELD_ID) {
+ spanId = SpanId.fromBytes(bytes, pos + ID_SIZE);
+ pos += ID_SIZE + SpanId.SIZE;
+ } else {
+ // TODO: update the spec to suggest that the span ID is not actually optional.
+ throw new SpanContextParseException("Invalid input: expected span ID at offset " + pos);
+ }
+ // Check to see if we are long enough to include an options field, and also that the next field
+ // is an options field. Per spec we simply stop parsing at first unknown field instead of
+ // failing.
+ if (bytes.length > pos && bytes[pos] == TRACE_OPTION_FIELD_ID) {
+ if (bytes.length < ALL_FORMAT_LENGTH) {
+ throw new SpanContextParseException("Invalid input: truncated");
+ }
+ traceOptions = TraceOptions.fromByte(bytes[pos + ID_SIZE]);
+ }
+ return SpanContext.create(traceId, spanId, traceOptions, TRACESTATE_DEFAULT);
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/PropagationComponentImpl.java b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/PropagationComponentImpl.java
new file mode 100644
index 00000000..f608543d
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/trace/propagation/PropagationComponentImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.trace.propagation;
+
+import io.opencensus.trace.propagation.BinaryFormat;
+import io.opencensus.trace.propagation.PropagationComponent;
+import io.opencensus.trace.propagation.TextFormat;
+
+/** Implementation of the {@link PropagationComponent}. */
+public class PropagationComponentImpl extends PropagationComponent {
+ private final BinaryFormat binaryFormat = new BinaryFormatImpl();
+ private final B3Format b3Format = new B3Format();
+
+ @Override
+ public BinaryFormat getBinaryFormat() {
+ return binaryFormat;
+ }
+
+ @Override
+ public TextFormat getB3Format() {
+ return b3Format;
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/internal/CurrentStateTest.java b/impl_core/src/test/java/io/opencensus/implcore/internal/CurrentStateTest.java
new file mode 100644
index 00000000..b7e6a93a
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/internal/CurrentStateTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018, 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.internal.CurrentState.State;
+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 CurrentState}. */
+@RunWith(JUnit4.class)
+public final class CurrentStateTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void defaultState() {
+ assertThat(new CurrentState(State.ENABLED).get()).isEqualTo(State.ENABLED);
+ }
+
+ @Test
+ public void setState() {
+ CurrentState currentState = new CurrentState(State.ENABLED);
+ assertThat(currentState.set(State.DISABLED)).isTrue();
+ assertThat(currentState.getInternal()).isEqualTo(State.DISABLED);
+ assertThat(currentState.set(State.ENABLED)).isTrue();
+ assertThat(currentState.getInternal()).isEqualTo(State.ENABLED);
+ assertThat(currentState.set(State.ENABLED)).isFalse();
+ }
+
+ @Test
+ public void preventSettingStateAfterReadingState() {
+ CurrentState currentState = new CurrentState(State.ENABLED);
+ currentState.get();
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ currentState.set(State.DISABLED);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/internal/TimestampConverterTest.java b/impl_core/src/test/java/io/opencensus/implcore/internal/TimestampConverterTest.java
new file mode 100644
index 00000000..32a3e687
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/internal/TimestampConverterTest.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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.common.Clock;
+import io.opencensus.common.Timestamp;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link TimestampConverter}. */
+@RunWith(JUnit4.class)
+public class TimestampConverterTest {
+ private final Timestamp timestamp = Timestamp.create(1234, 5678);
+ @Mock private Clock mockClock;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void convertNanoTime() {
+ when(mockClock.now()).thenReturn(timestamp);
+ when(mockClock.nowNanos()).thenReturn(1234L);
+ TimestampConverter timeConverter = TimestampConverter.now(mockClock);
+ assertThat(timeConverter.convertNanoTime(6234)).isEqualTo(Timestamp.create(1234, 10678));
+ assertThat(timeConverter.convertNanoTime(1000)).isEqualTo(Timestamp.create(1234, 5444));
+ assertThat(timeConverter.convertNanoTime(999995556)).isEqualTo(Timestamp.create(1235, 0));
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/internal/UtilsTest.java b/impl_core/src/test/java/io/opencensus/implcore/internal/UtilsTest.java
new file mode 100644
index 00000000..2e0bde21
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/internal/UtilsTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018, 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.internal;
+
+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 Utils}. */
+@RunWith(JUnit4.class)
+public class UtilsTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void checkListElementNull() {
+ List<Double> list = Arrays.asList(0.0, 1.0, 2.0, null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("null");
+ Utils.checkListElementNotNull(list, null);
+ }
+
+ @Test
+ public void checkListElementNull_WithMessage() {
+ List<Double> list = Arrays.asList(0.0, 1.0, 2.0, null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("list should not be null.");
+ Utils.checkListElementNotNull(list, "list should not be null.");
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImplTest.java
new file mode 100644
index 00000000..e69a284f
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedDoubleGaugeImplTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link DerivedDoubleGaugeImpl}. */
+@RunWith(JUnit4.class)
+public class DerivedDoubleGaugeImplTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String METRIC_NAME = "name";
+ private static final String METRIC_DESCRIPTION = "description";
+ private static final String METRIC_UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private static final List<LabelValue> LABEL_VALUES_1 =
+ Collections.singletonList(LabelValue.create("value1"));
+ private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+ private final TestClock testClock = TestClock.create(TEST_TIME);
+ private static final MetricDescriptor METRIC_DESCRIPTOR =
+ MetricDescriptor.create(
+ METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, Type.GAUGE_DOUBLE, LABEL_KEY);
+
+ private final DerivedDoubleGaugeImpl derivedDoubleGauge =
+ new DerivedDoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, LABEL_KEY);
+
+ // helper class
+ public static class QueueManager {
+ public double size() {
+ return 2.5;
+ }
+ }
+
+ private static final ToDoubleFunction<Object> doubleFunction =
+ new ToDoubleFunction<Object>() {
+ @Override
+ public double applyAsDouble(Object value) {
+ return 5.5;
+ }
+ };
+ private static final ToDoubleFunction<Object> negativeDoubleFunction =
+ new ToDoubleFunction<Object>() {
+ @Override
+ public double applyAsDouble(Object value) {
+ return -200.5;
+ }
+ };
+ private static final ToDoubleFunction<QueueManager> queueManagerFunction =
+ new ToDoubleFunction<QueueManager>() {
+ @Override
+ public double applyAsDouble(QueueManager queue) {
+ return queue.size();
+ }
+ };
+
+ @Test
+ public void createTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ derivedDoubleGauge.createTimeSeries(null, null, doubleFunction);
+ }
+
+ @Test
+ public void createTimeSeries_WithNullElement() {
+ List<LabelKey> labelKeys =
+ Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+ List<LabelValue> labelValues = Arrays.asList(LabelValue.create("value1"), null);
+ DerivedDoubleGaugeImpl derivedDoubleGauge =
+ new DerivedDoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValue element should not be null.");
+ derivedDoubleGauge.createTimeSeries(labelValues, null, doubleFunction);
+ }
+
+ @Test
+ public void createTimeSeries_WithInvalidLabelSize() {
+ List<LabelValue> labelValues =
+ Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Incorrect number of labels.");
+ derivedDoubleGauge.createTimeSeries(labelValues, null, doubleFunction);
+ }
+
+ @Test
+ public void createTimeSeries_WithNullFunction() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("function");
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, null);
+ }
+
+ @Test
+ public void createTimeSeries_WithObjFunction() {
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, new QueueManager(), queueManagerFunction);
+ Metric metric = derivedDoubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric)
+ .isEqualTo(
+ Metric.createWithOneTimeSeries(
+ METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(2.5), TEST_TIME), null)));
+ }
+
+ @Test
+ public void createTimeSeries_WithSameLabel() {
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, new QueueManager(), queueManagerFunction);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("A different time series with the same labels already exists.");
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, queueManagerFunction);
+ }
+
+ @Test
+ public void addTimeSeries_WithNullObj() {
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, negativeDoubleFunction);
+ Metric metric = derivedDoubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric)
+ .isEqualTo(
+ Metric.createWithOneTimeSeries(
+ METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(-200.5), TEST_TIME), null)));
+ }
+
+ @Test
+ public void removeTimeSeries() {
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+ Metric metric = derivedDoubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+ derivedDoubleGauge.removeTimeSeries(LABEL_VALUES);
+ assertThat(derivedDoubleGauge.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void removeTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ derivedDoubleGauge.removeTimeSeries(null);
+ }
+
+ @Test
+ public void multipleMetrics_GetMetric() {
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES_1, new QueueManager(), queueManagerFunction);
+ List<TimeSeries> expectedTimeSeriesList = new ArrayList<TimeSeries>();
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(5.5), TEST_TIME), null));
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES_1, Point.create(Value.doubleValue(2.5), TEST_TIME), null));
+ Metric metric = derivedDoubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+ assertThat(metric.getTimeSeriesList()).containsExactlyElementsIn(expectedTimeSeriesList);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(0))
+ .isEqualTo(LabelValue.create("value"));
+ assertThat(metric.getTimeSeriesList().get(1).getLabelValues().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(1).getLabelValues().get(0))
+ .isEqualTo(LabelValue.create("value1"));
+ }
+
+ @Test
+ public void clear() {
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES_1, new QueueManager(), queueManagerFunction);
+ Metric metric = derivedDoubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+ derivedDoubleGauge.clear();
+ assertThat(derivedDoubleGauge.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void empty_GetMetrics() {
+ assertThat(derivedDoubleGauge.getMetric(testClock)).isNull();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedLongGaugeImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedLongGaugeImplTest.java
new file mode 100644
index 00000000..ec9cad6c
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/DerivedLongGaugeImplTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link DerivedLongGaugeImpl}. */
+@RunWith(JUnit4.class)
+public class DerivedLongGaugeImplTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String METRIC_NAME = "name";
+ private static final String METRIC_DESCRIPTION = "description";
+ private static final String METRIC_UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private static final List<LabelValue> LABEL_VALUES_1 =
+ Collections.singletonList(LabelValue.create("value1"));
+
+ private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+ private final TestClock testClock = TestClock.create(TEST_TIME);
+
+ private static final MetricDescriptor METRIC_DESCRIPTOR =
+ MetricDescriptor.create(
+ METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, Type.GAUGE_INT64, LABEL_KEY);
+
+ private final DerivedLongGaugeImpl derivedLongGauge =
+ new DerivedLongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, LABEL_KEY);
+
+ // helper class
+ public static class QueueManager {
+ public long size() {
+ return 2;
+ }
+ }
+
+ private static final ToLongFunction<Object> longFunction =
+ new ToLongFunction<Object>() {
+ @Override
+ public long applyAsLong(Object value) {
+ return 5;
+ }
+ };
+ private static final ToLongFunction<Object> negativeLongFunction =
+ new ToLongFunction<Object>() {
+ @Override
+ public long applyAsLong(Object value) {
+ return -200;
+ }
+ };
+ private static final ToLongFunction<QueueManager> queueManagerFunction =
+ new ToLongFunction<QueueManager>() {
+ @Override
+ public long applyAsLong(QueueManager queue) {
+ return queue.size();
+ }
+ };
+
+ @Test
+ public void createTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ derivedLongGauge.createTimeSeries(null, null, longFunction);
+ }
+
+ @Test
+ public void createTimeSeries_WithNullElement() {
+ List<LabelKey> labelKeys =
+ Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+ List<LabelValue> labelValues = Arrays.asList(LabelValue.create("value1"), null);
+
+ DerivedLongGaugeImpl derivedLongGauge =
+ new DerivedLongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValue element should not be null.");
+ derivedLongGauge.createTimeSeries(labelValues, null, longFunction);
+ }
+
+ @Test
+ public void createTimeSeries_WithInvalidLabelSize() {
+ List<LabelValue> labelValues =
+ Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Incorrect number of labels.");
+ derivedLongGauge.createTimeSeries(labelValues, null, longFunction);
+ }
+
+ @Test
+ public void createTimeSeries_WithNullFunction() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("function");
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, null, null);
+ }
+
+ @Test
+ public void createTimeSeries_WithObjFunction() {
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, new QueueManager(), queueManagerFunction);
+
+ Metric metric = derivedLongGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric)
+ .isEqualTo(
+ Metric.createWithOneTimeSeries(
+ METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(2), TEST_TIME), null)));
+ }
+
+ @Test
+ public void addTimeSeries_WithNullObj() {
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, null, negativeLongFunction);
+
+ Metric metric = derivedLongGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric)
+ .isEqualTo(
+ Metric.createWithOneTimeSeries(
+ METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(-200), TEST_TIME), null)));
+ }
+
+ @Test
+ public void removeTimeSeries() {
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+ Metric metric = derivedLongGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+
+ derivedLongGauge.removeTimeSeries(LABEL_VALUES);
+ assertThat(derivedLongGauge.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void removeTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ derivedLongGauge.removeTimeSeries(null);
+ }
+
+ @Test
+ public void multipleMetrics_GetMetric() {
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+ derivedLongGauge.createTimeSeries(LABEL_VALUES_1, new QueueManager(), queueManagerFunction);
+
+ List<TimeSeries> expectedTimeSeriesList = new ArrayList<TimeSeries>();
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(5), TEST_TIME), null));
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES_1, Point.create(Value.longValue(2), TEST_TIME), null));
+
+ Metric metric = derivedLongGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+ assertThat(metric.getTimeSeriesList()).containsExactlyElementsIn(expectedTimeSeriesList);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(0))
+ .isEqualTo(LabelValue.create("value"));
+ assertThat(metric.getTimeSeriesList().get(1).getLabelValues().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(1).getLabelValues().get(0))
+ .isEqualTo(LabelValue.create("value1"));
+ }
+
+ @Test
+ public void clear() {
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+ derivedLongGauge.createTimeSeries(LABEL_VALUES_1, new QueueManager(), queueManagerFunction);
+
+ Metric metric = derivedLongGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+
+ derivedLongGauge.clear();
+ assertThat(derivedLongGauge.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void empty_GetMetrics() {
+ assertThat(derivedLongGauge.getMetric(testClock)).isNull();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/DoubleGaugeImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/DoubleGaugeImplTest.java
new file mode 100644
index 00000000..b0899084
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/DoubleGaugeImplTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.metrics.DoubleGaugeImpl.UNSET_VALUE;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.DoubleGauge.DoublePoint;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link DoubleGaugeImpl}. */
+@RunWith(JUnit4.class)
+public class DoubleGaugeImplTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String METRIC_NAME = "name";
+ private static final String METRIC_DESCRIPTION = "description";
+ private static final String METRIC_UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private static final List<LabelValue> LABEL_VALUES1 =
+ Collections.singletonList(LabelValue.create("value1"));
+ private static final List<LabelValue> DEFAULT_LABEL_VALUES =
+ Collections.singletonList(UNSET_VALUE);
+
+ private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+ private final TestClock testClock = TestClock.create(TEST_TIME);
+ private static final MetricDescriptor METRIC_DESCRIPTOR =
+ MetricDescriptor.create(
+ METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, Type.GAUGE_DOUBLE, LABEL_KEY);
+ private final DoubleGaugeImpl doubleGauge =
+ new DoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, LABEL_KEY);
+
+ @Test
+ public void getOrCreateTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ doubleGauge.getOrCreateTimeSeries(null);
+ }
+
+ @Test
+ public void getOrCreateTimeSeries_WithNullElement() {
+ List<LabelKey> labelKeys =
+ Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+ List<LabelValue> labelValues = Arrays.asList(LabelValue.create("value1"), null);
+
+ DoubleGaugeImpl doubleGauge =
+ new DoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValue element should not be null.");
+ doubleGauge.getOrCreateTimeSeries(labelValues);
+ }
+
+ @Test
+ public void getOrCreateTimeSeries_WithInvalidLabelSize() {
+ List<LabelValue> labelValues =
+ Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Incorrect number of labels.");
+ doubleGauge.getOrCreateTimeSeries(labelValues);
+ }
+
+ @Test
+ public void getOrCreateTimeSeries() {
+ DoublePoint point = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ point.add(100);
+ DoublePoint point1 = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ point1.set(500);
+
+ Metric metric = doubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric)
+ .isEqualTo(
+ Metric.create(
+ METRIC_DESCRIPTOR,
+ Collections.singletonList(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(500), TEST_TIME), null))));
+ assertThat(point).isSameAs(point1);
+ }
+
+ @Test
+ public void getOrCreateTimeSeries_WithNegativePointValues() {
+ DoublePoint point = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ point.add(-100);
+ point.add(-33);
+
+ Metric metric = doubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.doubleValue(-133));
+ assertThat(metric.getTimeSeriesList().get(0).getPoints().get(0).getTimestamp())
+ .isEqualTo(TEST_TIME);
+ assertThat(metric.getTimeSeriesList().get(0).getStartTimestamp()).isNull();
+ }
+
+ @Test
+ public void getDefaultTimeSeries() {
+ DoublePoint point = doubleGauge.getDefaultTimeSeries();
+ point.add(100);
+ point.set(500);
+
+ DoublePoint point1 = doubleGauge.getDefaultTimeSeries();
+ point1.add(-100);
+
+ Metric metric = doubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric)
+ .isEqualTo(
+ Metric.create(
+ METRIC_DESCRIPTOR,
+ Collections.singletonList(
+ TimeSeries.createWithOnePoint(
+ DEFAULT_LABEL_VALUES,
+ Point.create(Value.doubleValue(400), TEST_TIME),
+ null))));
+ assertThat(point).isSameAs(point1);
+ }
+
+ @Test
+ public void removeTimeSeries() {
+ doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ assertThat(doubleGauge.getMetric(testClock))
+ .isEqualTo(
+ Metric.create(
+ METRIC_DESCRIPTOR,
+ Collections.singletonList(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(0), TEST_TIME), null))));
+
+ doubleGauge.removeTimeSeries(LABEL_VALUES);
+ assertThat(doubleGauge.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void removeTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ doubleGauge.removeTimeSeries(null);
+ }
+
+ @Test
+ public void clear() {
+ DoublePoint doublePoint = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ doublePoint.add(-11);
+ DoublePoint defaultPoint = doubleGauge.getDefaultTimeSeries();
+ defaultPoint.set(100);
+
+ Metric metric = doubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+
+ doubleGauge.clear();
+ assertThat(doubleGauge.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void setDefaultLabelValues() {
+ List<LabelKey> labelKeys =
+ Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+ DoubleGaugeImpl doubleGauge =
+ new DoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+ DoublePoint defaultPoint = doubleGauge.getDefaultTimeSeries();
+ defaultPoint.set(-230);
+
+ Metric metric = doubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(2);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(0)).isEqualTo(UNSET_VALUE);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(1)).isEqualTo(UNSET_VALUE);
+ }
+
+ @Test
+ public void pointImpl_InstanceOf() {
+ DoublePoint doublePoint = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ assertThat(doublePoint).isInstanceOf(DoubleGaugeImpl.PointImpl.class);
+ }
+
+ @Test
+ public void multipleMetrics_GetMetric() {
+ DoublePoint doublePoint = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ doublePoint.add(1);
+ doublePoint.add(2);
+
+ DoublePoint defaultPoint = doubleGauge.getDefaultTimeSeries();
+ defaultPoint.set(100);
+
+ DoublePoint doublePoint1 = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES1);
+ doublePoint1.add(-100);
+ doublePoint1.add(-20);
+
+ List<TimeSeries> expectedTimeSeriesList = new ArrayList<TimeSeries>();
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(3), TEST_TIME), null));
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ DEFAULT_LABEL_VALUES, Point.create(Value.doubleValue(100), TEST_TIME), null));
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES1, Point.create(Value.doubleValue(-120), TEST_TIME), null));
+
+ Metric metric = doubleGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(3);
+ assertThat(metric.getTimeSeriesList()).containsExactlyElementsIn(expectedTimeSeriesList);
+ }
+
+ @Test
+ public void empty_GetMetrics() {
+ assertThat(doubleGauge.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void testEquals() {
+ List<LabelKey> labelKeys =
+ Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+ List<LabelValue> labelValues =
+ Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+ DoubleGaugeImpl doubleGauge =
+ new DoubleGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+
+ DoublePoint defaultPoint1 = doubleGauge.getDefaultTimeSeries();
+ DoublePoint defaultPoint2 = doubleGauge.getDefaultTimeSeries();
+ DoublePoint doublePoint1 = doubleGauge.getOrCreateTimeSeries(labelValues);
+ DoublePoint doublePoint2 = doubleGauge.getOrCreateTimeSeries(labelValues);
+
+ new EqualsTester()
+ .addEqualityGroup(defaultPoint1, defaultPoint2)
+ .addEqualityGroup(doublePoint1, doublePoint2)
+ .testEquals();
+
+ doubleGauge.clear();
+
+ DoublePoint newDefaultPointAfterClear = doubleGauge.getDefaultTimeSeries();
+ DoublePoint newDoublePointAfterClear = doubleGauge.getOrCreateTimeSeries(labelValues);
+
+ doubleGauge.removeTimeSeries(labelValues);
+ DoublePoint newDoublePointAfterRemove = doubleGauge.getOrCreateTimeSeries(labelValues);
+
+ new EqualsTester()
+ .addEqualityGroup(defaultPoint1, defaultPoint2)
+ .addEqualityGroup(doublePoint1, doublePoint2)
+ .addEqualityGroup(newDefaultPointAfterClear)
+ .addEqualityGroup(newDoublePointAfterClear)
+ .addEqualityGroup(newDoublePointAfterRemove)
+ .testEquals();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/LongGaugeImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/LongGaugeImplTest.java
new file mode 100644
index 00000000..e83bb642
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/LongGaugeImplTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.metrics.LongGaugeImpl.UNSET_VALUE;
+
+import com.google.common.testing.EqualsTester;
+import io.opencensus.common.Timestamp;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.LongGauge.LongPoint;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.ArrayList;
+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;
+
+/** Unit tests for {@link LongGaugeImpl}. */
+@RunWith(JUnit4.class)
+public class LongGaugeImplTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String METRIC_NAME = "name";
+ private static final String METRIC_DESCRIPTION = "description";
+ private static final String METRIC_UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+ private static final List<LabelValue> LABEL_VALUES1 =
+ Collections.singletonList(LabelValue.create("value1"));
+ private static final List<LabelValue> DEFAULT_LABEL_VALUES =
+ Collections.singletonList(UNSET_VALUE);
+
+ private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+ private final TestClock testClock = TestClock.create(TEST_TIME);
+ private static final MetricDescriptor METRIC_DESCRIPTOR =
+ MetricDescriptor.create(
+ METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, Type.GAUGE_INT64, LABEL_KEY);
+ private final LongGaugeImpl longGaugeMetric =
+ new LongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, LABEL_KEY);
+
+ @Test
+ public void getOrCreateTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ longGaugeMetric.getOrCreateTimeSeries(null);
+ }
+
+ @Test
+ public void getOrCreateTimeSeries_WithNullElement() {
+ List<LabelKey> labelKeys =
+ Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+ List<LabelValue> labelValues = Arrays.asList(LabelValue.create("value1"), null);
+
+ LongGaugeImpl longGauge =
+ new LongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValue element should not be null.");
+ longGauge.getOrCreateTimeSeries(labelValues);
+ }
+
+ @Test
+ public void getOrCreateTimeSeries_WithInvalidLabelSize() {
+ List<LabelValue> labelValues =
+ Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("Incorrect number of labels.");
+ longGaugeMetric.getOrCreateTimeSeries(labelValues);
+ }
+
+ @Test
+ public void getOrCreateTimeSeries() {
+ LongPoint point = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+ point.add(100);
+ LongPoint point1 = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+ point1.set(500);
+
+ Metric metric = longGaugeMetric.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric)
+ .isEqualTo(
+ Metric.createWithOneTimeSeries(
+ METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(500), TEST_TIME), null)));
+ assertThat(point).isSameAs(point1);
+ }
+
+ @Test
+ public void getOrCreateTimeSeries_WithNegativePointValues() {
+ LongPoint point = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+ point.add(-100);
+ point.add(-33);
+
+ Metric metric = longGaugeMetric.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(0).getPoints().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(0).getPoints().get(0).getValue())
+ .isEqualTo(Value.longValue(-133));
+ assertThat(metric.getTimeSeriesList().get(0).getPoints().get(0).getTimestamp())
+ .isEqualTo(TEST_TIME);
+ assertThat(metric.getTimeSeriesList().get(0).getStartTimestamp()).isNull();
+ }
+
+ @Test
+ public void getDefaultTimeSeries() {
+ LongPoint point = longGaugeMetric.getDefaultTimeSeries();
+ point.add(100);
+ point.set(500);
+
+ LongPoint point1 = longGaugeMetric.getDefaultTimeSeries();
+ point1.add(-100);
+
+ Metric metric = longGaugeMetric.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric)
+ .isEqualTo(
+ Metric.createWithOneTimeSeries(
+ METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ DEFAULT_LABEL_VALUES, Point.create(Value.longValue(400), TEST_TIME), null)));
+ assertThat(point).isSameAs(point1);
+ }
+
+ @Test
+ public void removeTimeSeries() {
+ longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+ assertThat(longGaugeMetric.getMetric(testClock))
+ .isEqualTo(
+ Metric.createWithOneTimeSeries(
+ METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(0), TEST_TIME), null)));
+
+ longGaugeMetric.removeTimeSeries(LABEL_VALUES);
+ assertThat(longGaugeMetric.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void removeTimeSeries_WithNullLabelValues() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelValues");
+ longGaugeMetric.removeTimeSeries(null);
+ }
+
+ @Test
+ public void clear() {
+ LongPoint longPoint = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+ longPoint.add(-11);
+ LongPoint defaultPoint = longGaugeMetric.getDefaultTimeSeries();
+ defaultPoint.set(100);
+
+ Metric metric = longGaugeMetric.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(2);
+
+ longGaugeMetric.clear();
+ assertThat(longGaugeMetric.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void setDefaultLabelValues() {
+ List<LabelKey> labelKeys =
+ Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+ LongGaugeImpl longGauge =
+ new LongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+ LongPoint defaultPoint = longGauge.getDefaultTimeSeries();
+ defaultPoint.set(-230);
+
+ Metric metric = longGauge.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(1);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().size()).isEqualTo(2);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(0)).isEqualTo(UNSET_VALUE);
+ assertThat(metric.getTimeSeriesList().get(0).getLabelValues().get(1)).isEqualTo(UNSET_VALUE);
+ }
+
+ @Test
+ public void pointImpl_InstanceOf() {
+ LongPoint longPoint = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+ assertThat(longPoint).isInstanceOf(LongGaugeImpl.PointImpl.class);
+ }
+
+ @Test
+ public void multipleMetrics_GetMetric() {
+ LongPoint longPoint = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES);
+ longPoint.add(1);
+ longPoint.add(2);
+
+ LongPoint defaultPoint = longGaugeMetric.getDefaultTimeSeries();
+ defaultPoint.set(100);
+
+ LongPoint longPoint1 = longGaugeMetric.getOrCreateTimeSeries(LABEL_VALUES1);
+ longPoint1.add(-100);
+ longPoint1.add(-20);
+
+ List<TimeSeries> expectedTimeSeriesList = new ArrayList<TimeSeries>();
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(3), TEST_TIME), null));
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ DEFAULT_LABEL_VALUES, Point.create(Value.longValue(100), TEST_TIME), null));
+ expectedTimeSeriesList.add(
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES1, Point.create(Value.longValue(-120), TEST_TIME), null));
+
+ Metric metric = longGaugeMetric.getMetric(testClock);
+ assertThat(metric).isNotNull();
+ assertThat(metric.getMetricDescriptor()).isEqualTo(METRIC_DESCRIPTOR);
+ assertThat(metric.getTimeSeriesList().size()).isEqualTo(3);
+ assertThat(metric.getTimeSeriesList()).containsExactlyElementsIn(expectedTimeSeriesList);
+ }
+
+ @Test
+ public void empty_GetMetrics() {
+ assertThat(longGaugeMetric.getMetric(testClock)).isNull();
+ }
+
+ @Test
+ public void testEquals() {
+ List<LabelKey> labelKeys =
+ Arrays.asList(LabelKey.create("key1", "desc"), LabelKey.create("key2", "desc"));
+ List<LabelValue> labelValues =
+ Arrays.asList(LabelValue.create("value1"), LabelValue.create("value2"));
+
+ LongGaugeImpl longGauge =
+ new LongGaugeImpl(METRIC_NAME, METRIC_DESCRIPTION, METRIC_UNIT, labelKeys);
+
+ LongPoint defaultPoint1 = longGauge.getDefaultTimeSeries();
+ LongPoint defaultPoint2 = longGauge.getDefaultTimeSeries();
+ LongPoint longPoint1 = longGauge.getOrCreateTimeSeries(labelValues);
+ LongPoint longPoint2 = longGauge.getOrCreateTimeSeries(labelValues);
+
+ new EqualsTester()
+ .addEqualityGroup(defaultPoint1, defaultPoint2)
+ .addEqualityGroup(longPoint1, longPoint2)
+ .testEquals();
+
+ longGauge.clear();
+
+ LongPoint newDefaultPointAfterClear = longGauge.getDefaultTimeSeries();
+ LongPoint newLongPointAfterClear = longGauge.getOrCreateTimeSeries(labelValues);
+
+ longGauge.removeTimeSeries(labelValues);
+ LongPoint newLongPointAfterRemove = longGauge.getOrCreateTimeSeries(labelValues);
+
+ new EqualsTester()
+ .addEqualityGroup(defaultPoint1, defaultPoint2)
+ .addEqualityGroup(longPoint1, longPoint2)
+ .addEqualityGroup(newDefaultPointAfterClear)
+ .addEqualityGroup(newLongPointAfterClear)
+ .addEqualityGroup(newLongPointAfterRemove)
+ .testEquals();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricRegistryImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricRegistryImplTest.java
new file mode 100644
index 00000000..68bfda31
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricRegistryImplTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.common.ToDoubleFunction;
+import io.opencensus.common.ToLongFunction;
+import io.opencensus.metrics.DerivedDoubleGauge;
+import io.opencensus.metrics.DerivedLongGauge;
+import io.opencensus.metrics.DoubleGauge;
+import io.opencensus.metrics.DoubleGauge.DoublePoint;
+import io.opencensus.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.LongGauge;
+import io.opencensus.metrics.LongGauge.LongPoint;
+import io.opencensus.metrics.export.Metric;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.TimeSeries;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.testing.common.TestClock;
+import java.util.Collection;
+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;
+
+/** Unit tests for {@link MetricRegistryImpl}. */
+@RunWith(JUnit4.class)
+public class MetricRegistryImplTest {
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final String NAME = "name";
+ private static final String NAME_2 = "name2";
+ private static final String NAME_3 = "name3";
+ private static final String NAME_4 = "name4";
+ private static final String DESCRIPTION = "description";
+ private static final String UNIT = "1";
+ private static final List<LabelKey> LABEL_KEY =
+ Collections.singletonList(LabelKey.create("key", "key description"));
+ private static final List<LabelValue> LABEL_VALUES =
+ Collections.singletonList(LabelValue.create("value"));
+
+ private static final Timestamp TEST_TIME = Timestamp.create(1234, 123);
+ private final TestClock testClock = TestClock.create(TEST_TIME);
+ private final MetricRegistryImpl metricRegistry = new MetricRegistryImpl(testClock);
+
+ private static final MetricDescriptor LONG_METRIC_DESCRIPTOR =
+ MetricDescriptor.create(NAME, DESCRIPTION, UNIT, Type.GAUGE_INT64, LABEL_KEY);
+ private static final MetricDescriptor DOUBLE_METRIC_DESCRIPTOR =
+ MetricDescriptor.create(NAME_2, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, LABEL_KEY);
+ private static final MetricDescriptor DERIVED_LONG_METRIC_DESCRIPTOR =
+ MetricDescriptor.create(NAME_3, DESCRIPTION, UNIT, Type.GAUGE_INT64, LABEL_KEY);
+ private static final MetricDescriptor DERIVED_DOUBLE_METRIC_DESCRIPTOR =
+ MetricDescriptor.create(NAME_4, DESCRIPTION, UNIT, Type.GAUGE_DOUBLE, LABEL_KEY);
+
+ private static final ToLongFunction<Object> longFunction =
+ new ToLongFunction<Object>() {
+ @Override
+ public long applyAsLong(Object value) {
+ return 5;
+ }
+ };
+ private static final ToDoubleFunction<Object> doubleFunction =
+ new ToDoubleFunction<Object>() {
+ @Override
+ public double applyAsDouble(Object value) {
+ return 5.0;
+ }
+ };
+
+ @Test
+ public void addLongGauge_NullName() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("name");
+ metricRegistry.addLongGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void addLongGauge_NullDescription() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("description");
+ metricRegistry.addLongGauge(NAME, null, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void addLongGauge_NullUnit() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("unit");
+ metricRegistry.addLongGauge(NAME, DESCRIPTION, null, LABEL_KEY);
+ }
+
+ @Test
+ public void addLongGauge_NullLabels() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKeys");
+ metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, null);
+ }
+
+ @Test
+ public void addLongGauge_WithNullElement() {
+ List<LabelKey> labelKeys = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKey element should not be null.");
+ metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, labelKeys);
+ }
+
+ @Test
+ public void addDoubleGauge_NullName() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("name");
+ metricRegistry.addDoubleGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void addDoubleGauge_NullDescription() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("description");
+ metricRegistry.addDoubleGauge(NAME_2, null, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void addDoubleGauge_NullUnit() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("unit");
+ metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, null, LABEL_KEY);
+ }
+
+ @Test
+ public void addDoubleGauge_NullLabels() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKeys");
+ metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, null);
+ }
+
+ @Test
+ public void addDoubleGauge_WithNullElement() {
+ List<LabelKey> labelKeys = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKey element should not be null.");
+ metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, labelKeys);
+ }
+
+ @Test
+ public void addDerivedLongGauge_NullName() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("name");
+ metricRegistry.addDerivedLongGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void addDerivedLongGauge_NullDescription() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("description");
+ metricRegistry.addDerivedLongGauge(NAME_3, null, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void addDerivedLongGauge_NullUnit() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("unit");
+ metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, null, LABEL_KEY);
+ }
+
+ @Test
+ public void addDerivedLongGauge_NullLabels() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKeys");
+ metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, null);
+ }
+
+ @Test
+ public void addDerivedLongGauge_WithNullElement() {
+ List<LabelKey> labelKeys = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKey element should not be null.");
+ metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, labelKeys);
+ }
+
+ @Test
+ public void addDerivedDoubleGauge_NullName() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("name");
+ metricRegistry.addDerivedDoubleGauge(null, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void addDerivedDoubleGauge_NullDescription() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("description");
+ metricRegistry.addDerivedDoubleGauge(NAME_4, null, UNIT, LABEL_KEY);
+ }
+
+ @Test
+ public void addDerivedDoubleGauge_NullUnit() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("unit");
+ metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, null, LABEL_KEY);
+ }
+
+ @Test
+ public void addDerivedDoubleGauge_NullLabels() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKeys");
+ metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, null);
+ }
+
+ @Test
+ public void addDerivedDoubleGauge_WithNullElement() {
+ List<LabelKey> labelKeys = Collections.singletonList(null);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("labelKey element should not be null.");
+ metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, labelKeys);
+ }
+
+ @Test
+ public void addLongGauge_GetMetrics() {
+ LongGauge longGauge = metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ longGauge.getOrCreateTimeSeries(LABEL_VALUES);
+
+ Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+ assertThat(metricCollections.size()).isEqualTo(1);
+ assertThat(metricCollections)
+ .containsExactly(
+ Metric.createWithOneTimeSeries(
+ LONG_METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(0), TEST_TIME), null)));
+ }
+
+ @Test
+ public void addDoubleGauge_GetMetrics() {
+ DoubleGauge doubleGauge = metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY);
+ doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+ assertThat(metricCollections.size()).isEqualTo(1);
+ assertThat(metricCollections)
+ .containsExactly(
+ Metric.createWithOneTimeSeries(
+ DOUBLE_METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(0.0), TEST_TIME), null)));
+ }
+
+ @Test
+ public void addDerivedLongGauge_GetMetrics() {
+ DerivedLongGauge derivedLongGauge =
+ metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY);
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+ Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+ assertThat(metricCollections.size()).isEqualTo(1);
+ assertThat(metricCollections)
+ .containsExactly(
+ Metric.createWithOneTimeSeries(
+ DERIVED_LONG_METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(5), TEST_TIME), null)));
+ }
+
+ @Test
+ public void addDerivedDoubleGauge_GetMetrics() {
+ DerivedDoubleGauge derivedDoubleGauge =
+ metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY);
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+ Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+ assertThat(metricCollections.size()).isEqualTo(1);
+ assertThat(metricCollections)
+ .containsExactly(
+ Metric.createWithOneTimeSeries(
+ DERIVED_DOUBLE_METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(5.0), TEST_TIME), null)));
+ }
+
+ @Test
+ public void empty_GetMetrics() {
+ assertThat(metricRegistry.getMetricProducer().getMetrics()).isEmpty();
+ }
+
+ @Test
+ public void checkInstanceOf() {
+ assertThat(metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY))
+ .isInstanceOf(LongGaugeImpl.class);
+ assertThat(metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY))
+ .isInstanceOf(DoubleGaugeImpl.class);
+ assertThat(metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY))
+ .isInstanceOf(DerivedLongGaugeImpl.class);
+ assertThat(metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY))
+ .isInstanceOf(DerivedDoubleGaugeImpl.class);
+ }
+
+ @Test
+ public void getMetrics() {
+ LongGauge longGauge = metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ LongPoint longPoint = longGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ longPoint.set(200);
+ DoubleGauge doubleGauge = metricRegistry.addDoubleGauge(NAME_2, DESCRIPTION, UNIT, LABEL_KEY);
+ DoublePoint doublePoint = doubleGauge.getOrCreateTimeSeries(LABEL_VALUES);
+ doublePoint.set(-300.13);
+ DerivedLongGauge derivedLongGauge =
+ metricRegistry.addDerivedLongGauge(NAME_3, DESCRIPTION, UNIT, LABEL_KEY);
+ derivedLongGauge.createTimeSeries(LABEL_VALUES, null, longFunction);
+ DerivedDoubleGauge derivedDoubleGauge =
+ metricRegistry.addDerivedDoubleGauge(NAME_4, DESCRIPTION, UNIT, LABEL_KEY);
+ derivedDoubleGauge.createTimeSeries(LABEL_VALUES, null, doubleFunction);
+
+ Collection<Metric> metricCollections = metricRegistry.getMetricProducer().getMetrics();
+ assertThat(metricCollections.size()).isEqualTo(4);
+ assertThat(metricCollections)
+ .containsExactly(
+ Metric.createWithOneTimeSeries(
+ LONG_METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(200), TEST_TIME), null)),
+ Metric.createWithOneTimeSeries(
+ DOUBLE_METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(-300.13), TEST_TIME), null)),
+ Metric.createWithOneTimeSeries(
+ DERIVED_LONG_METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.longValue(5), TEST_TIME), null)),
+ Metric.createWithOneTimeSeries(
+ DERIVED_DOUBLE_METRIC_DESCRIPTOR,
+ TimeSeries.createWithOnePoint(
+ LABEL_VALUES, Point.create(Value.doubleValue(5.0), TEST_TIME), null)));
+ }
+
+ @Test
+ public void registerDifferentMetricSameName() {
+ metricRegistry.addLongGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("A different metric with the same name already registered.");
+ metricRegistry.addDoubleGauge(NAME, DESCRIPTION, UNIT, LABEL_KEY);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricsComponentImplBaseTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricsComponentImplBaseTest.java
new file mode 100644
index 00000000..7f8515d3
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/MetricsComponentImplBaseTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2018, 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.metrics.export.ExportComponentImpl;
+import io.opencensus.testing.common.TestClock;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link MetricsComponentImplBase}. */
+@RunWith(JUnit4.class)
+public class MetricsComponentImplBaseTest {
+ private final MetricsComponentImplBase metricsComponentImplBase =
+ new MetricsComponentImplBase(TestClock.create());
+
+ @Test
+ public void getExportComponent() {
+ assertThat(metricsComponentImplBase.getExportComponent())
+ .isInstanceOf(ExportComponentImpl.class);
+ }
+
+ @Test
+ public void getMetricRegistry() {
+ assertThat(metricsComponentImplBase.getMetricRegistry()).isInstanceOf(MetricRegistryImpl.class);
+ }
+
+ @Test
+ public void metricRegistry_InstalledToMetricProducerManger() {
+ assertThat(
+ metricsComponentImplBase
+ .getExportComponent()
+ .getMetricProducerManager()
+ .getAllMetricProducer())
+ .containsExactly(metricsComponentImplBase.getMetricRegistry().getMetricProducer());
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/export/ExportComponentImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/export/ExportComponentImplTest.java
new file mode 100644
index 00000000..fb91641c
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/export/ExportComponentImplTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExportComponentImpl}. */
+@RunWith(JUnit4.class)
+public class ExportComponentImplTest {
+
+ @Test
+ public void getMetricProducerManager() {
+ ExportComponentImpl exportComponent = new ExportComponentImpl();
+ assertThat(exportComponent.getMetricProducerManager())
+ .isInstanceOf(MetricProducerManagerImpl.class);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImplTest.java
new file mode 100644
index 00000000..e549dadb
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/metrics/export/MetricProducerManagerImplTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2018, 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.metrics.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.metrics.export.MetricProducer;
+import io.opencensus.metrics.export.MetricProducerManager;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link MetricProducerManagerImpl}. */
+@RunWith(JUnit4.class)
+public class MetricProducerManagerImplTest {
+
+ private final MetricProducerManager metricProducerManager = new MetricProducerManagerImpl();
+ @Mock private MetricProducer metricProducer;
+ @Mock private MetricProducer metricProducerOther;
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void add_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ metricProducerManager.add(null);
+ }
+
+ @Test
+ public void add() {
+ metricProducerManager.add(metricProducer);
+ assertThat(metricProducerManager.getAllMetricProducer()).containsExactly(metricProducer);
+ }
+
+ @Test
+ public void add_DuplicateElement() {
+ metricProducerManager.add(metricProducer);
+ Set<MetricProducer> metricProducerSet = metricProducerManager.getAllMetricProducer();
+ assertThat(metricProducerSet).containsExactly(metricProducer);
+ metricProducerManager.add(metricProducer);
+ // Returns the same object.
+ assertThat(metricProducerManager.getAllMetricProducer()).isSameAs(metricProducerSet);
+ }
+
+ @Test
+ public void add_MultipleElements() {
+ metricProducerManager.add(metricProducer);
+ Set<MetricProducer> metricProducerSet = metricProducerManager.getAllMetricProducer();
+ assertThat(metricProducerSet).containsExactly(metricProducer);
+ metricProducerManager.add(metricProducerOther);
+ // Returns the same object.
+ assertThat(metricProducerManager.getAllMetricProducer())
+ .containsExactly(metricProducer, metricProducerOther);
+ }
+
+ @Test
+ public void addAndRemove() {
+ metricProducerManager.add(metricProducer);
+ assertThat(metricProducerManager.getAllMetricProducer()).containsExactly(metricProducer);
+ metricProducerManager.remove(metricProducer);
+ assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+ }
+
+ @Test
+ public void remove_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ metricProducerManager.remove(null);
+ }
+
+ @Test
+ public void remove_FromEmpty() {
+ metricProducerManager.remove(metricProducer);
+ assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+ }
+
+ @Test
+ public void remove_NotPresent() {
+ metricProducerManager.add(metricProducer);
+ Set<MetricProducer> metricProducerSet = metricProducerManager.getAllMetricProducer();
+ assertThat(metricProducerSet).containsExactly(metricProducer);
+ metricProducerManager.remove(metricProducerOther);
+ // Returns the same object.
+ assertThat(metricProducerManager.getAllMetricProducer()).isSameAs(metricProducerSet);
+ }
+
+ @Test
+ public void getAllMetricProducer_empty() {
+ assertThat(metricProducerManager.getAllMetricProducer()).isEmpty();
+ }
+}
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..39a53e1a
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.stats.Measure.MeasureDouble;
+import io.opencensus.tags.TagValue;
+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 IntervalBucket}. */
+@RunWith(JUnit4.class)
+public class IntervalBucketTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final double TOLERANCE = 1e-6;
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure1", "description", "1");
+ 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, MEASURE_DOUBLE);
+ }
+
+ @Test
+ public void preventNullDuration() {
+ thrown.expect(NullPointerException.class);
+ new IntervalBucket(START, null, MEAN, MEASURE_DOUBLE);
+ }
+
+ @Test
+ public void preventNegativeDuration() {
+ thrown.expect(IllegalArgumentException.class);
+ new IntervalBucket(START, NEGATIVE_TEN_SEC, MEAN, MEASURE_DOUBLE);
+ }
+
+ @Test
+ public void preventNullAggregation() {
+ thrown.expect(NullPointerException.class);
+ new IntervalBucket(START, MINUTE, null, MEASURE_DOUBLE);
+ }
+
+ @Test
+ public void preventNullMeasure() {
+ thrown.expect(NullPointerException.class);
+ new IntervalBucket(START, MINUTE, MEAN, null);
+ }
+
+ @Test
+ public void testGetTagValueAggregationMap_empty() {
+ assertThat(new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE).getTagValueAggregationMap())
+ .isEmpty();
+ }
+
+ @Test
+ public void testGetStart() {
+ assertThat(new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE).getStart()).isEqualTo(START);
+ }
+
+ @Test
+ public void testRecord() {
+ IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE);
+ List<TagValue> tagValues1 = Arrays.<TagValue>asList(TagValue.create("VALUE1"));
+ List<TagValue> tagValues2 = Arrays.<TagValue>asList(TagValue.create("VALUE2"));
+ bucket.record(tagValues1, 5.0, Collections.<String, String>emptyMap(), START);
+ bucket.record(tagValues1, 15.0, Collections.<String, String>emptyMap(), START);
+ bucket.record(tagValues2, 10.0, Collections.<String, String>emptyMap(), START);
+ 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, MEASURE_DOUBLE)
+ .getFraction(thirtySecondsAfterStart))
+ .isWithin(TOLERANCE)
+ .of(0.5);
+ }
+
+ @Test
+ public void preventCallingGetFractionOnPastBuckets() {
+ IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE);
+ Timestamp twoMinutesAfterStart = Timestamp.create(180, 0);
+ thrown.expect(IllegalArgumentException.class);
+ bucket.getFraction(twoMinutesAfterStart);
+ }
+
+ @Test
+ public void preventCallingGetFractionOnFutureBuckets() {
+ IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN, MEASURE_DOUBLE);
+ 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..19e8a6c5
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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 testPutAttachment() {
+ MeasureMapInternal metrics =
+ MeasureMapInternal.builder()
+ .putAttachment("k1", "v1")
+ .putAttachment("k2", "v2")
+ .putAttachment("k1", "v3")
+ .build();
+ assertThat(metrics.getAttachments()).containsExactly("k1", "v3", "k2", "v2");
+ assertContains(metrics);
+ }
+
+ @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..25f33a94
--- /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.implcore.internal.CurrentState.State;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Measure;
+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, State.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/MetricUtilsTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MetricUtilsTest.java
new file mode 100644
index 00000000..66e971f6
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MetricUtilsTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2018, 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.metrics.LabelKey;
+import io.opencensus.metrics.LabelValue;
+import io.opencensus.metrics.export.MetricDescriptor;
+import io.opencensus.metrics.export.MetricDescriptor.Type;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MetricUtils}. */
+@RunWith(JUnit4.class)
+public class MetricUtilsTest {
+
+ 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 Duration TEN_SECONDS = Duration.create(10, 0);
+ private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(-10.0, 0.0, 10.0));
+ private static final Sum SUM = Sum.create();
+ private static final Count COUNT = Count.create();
+ private static final Mean MEAN = Mean.create();
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+ private static final LastValue LAST_VALUE = LastValue.create();
+ private static final View VIEW_1 =
+ View.create(
+ VIEW_NAME, VIEW_DESCRIPTION, MEASURE_DOUBLE, LAST_VALUE, Collections.singletonList(KEY));
+ private static final View VIEW_2 =
+ View.create(
+ VIEW_NAME_2,
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ MEAN,
+ Collections.singletonList(KEY),
+ INTERVAL);
+ private static final Timestamp TIMESTAMP = Timestamp.fromMillis(1000);
+
+ @Test
+ public void viewToMetricDescriptor() {
+ MetricDescriptor metricDescriptor = MetricUtils.viewToMetricDescriptor(VIEW_1);
+ assertThat(metricDescriptor).isNotNull();
+ assertThat(metricDescriptor.getName()).isEqualTo(VIEW_NAME.asString());
+ assertThat(metricDescriptor.getUnit()).isEqualTo(MEASURE_UNIT);
+ assertThat(metricDescriptor.getType()).isEqualTo(Type.GAUGE_DOUBLE);
+ assertThat(metricDescriptor.getDescription()).isEqualTo(VIEW_DESCRIPTION);
+ assertThat(metricDescriptor.getLabelKeys()).containsExactly(LabelKey.create(KEY.getName(), ""));
+ }
+
+ @Test
+ public void viewToMetricDescriptor_NoIntervalViews() {
+ MetricDescriptor metricDescriptor = MetricUtils.viewToMetricDescriptor(VIEW_2);
+ assertThat(metricDescriptor).isNull();
+ }
+
+ @Test
+ public void getType() {
+ assertThat(MetricUtils.getType(MEASURE_DOUBLE, LAST_VALUE)).isEqualTo(Type.GAUGE_DOUBLE);
+ assertThat(MetricUtils.getType(MEASURE_LONG, LAST_VALUE)).isEqualTo(Type.GAUGE_INT64);
+ assertThat(MetricUtils.getType(MEASURE_DOUBLE, SUM)).isEqualTo(Type.CUMULATIVE_DOUBLE);
+ assertThat(MetricUtils.getType(MEASURE_LONG, SUM)).isEqualTo(Type.CUMULATIVE_INT64);
+ assertThat(MetricUtils.getType(MEASURE_DOUBLE, MEAN)).isEqualTo(Type.CUMULATIVE_DOUBLE);
+ assertThat(MetricUtils.getType(MEASURE_LONG, MEAN)).isEqualTo(Type.CUMULATIVE_DOUBLE);
+ assertThat(MetricUtils.getType(MEASURE_DOUBLE, COUNT)).isEqualTo(Type.CUMULATIVE_INT64);
+ assertThat(MetricUtils.getType(MEASURE_LONG, COUNT)).isEqualTo(Type.CUMULATIVE_INT64);
+ assertThat(MetricUtils.getType(MEASURE_DOUBLE, DISTRIBUTION))
+ .isEqualTo(Type.CUMULATIVE_DISTRIBUTION);
+ assertThat(MetricUtils.getType(MEASURE_LONG, DISTRIBUTION))
+ .isEqualTo(Type.CUMULATIVE_DISTRIBUTION);
+ }
+
+ @Test
+ public void tagValuesToLabelValues() {
+ List<TagValue> tagValues = Arrays.asList(VALUE, VALUE_2, null);
+ assertThat(MetricUtils.tagValuesToLabelValues(tagValues))
+ .containsExactly(
+ LabelValue.create(VALUE.asString()),
+ LabelValue.create(VALUE_2.asString()),
+ LabelValue.create(null));
+ }
+}
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..a6139e53
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java
@@ -0,0 +1,339 @@
+/*
+ * 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.assertAggregationDataEquals;
+
+import com.google.common.collect.ImmutableList;
+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.MutableLastValueDouble;
+import io.opencensus.implcore.stats.MutableAggregation.MutableLastValueLong;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSumDouble;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSumLong;
+import io.opencensus.metrics.export.Distribution;
+import io.opencensus.metrics.export.Distribution.Bucket;
+import io.opencensus.metrics.export.Distribution.BucketOptions;
+import io.opencensus.metrics.export.Point;
+import io.opencensus.metrics.export.Value;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+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));
+ private static final BucketBoundaries BUCKET_BOUNDARIES_EMPTY =
+ BucketBoundaries.create(Collections.<Double>emptyList());
+ private static final Timestamp TIMESTAMP = Timestamp.create(60, 0);
+
+ @Test
+ public void testCreateEmpty() {
+ assertThat(MutableSumDouble.create().getSum()).isWithin(TOLERANCE).of(0);
+ assertThat(MutableSumLong.create().getSum()).isWithin(TOLERANCE).of(0);
+ assertThat(MutableCount.create().getCount()).isEqualTo(0);
+ assertThat(MutableMean.create().getMean()).isWithin(TOLERANCE).of(0);
+ assertThat(MutableLastValueDouble.create().getLastValue()).isNaN();
+ assertThat(MutableLastValueLong.create().getLastValue()).isNaN();
+
+ 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]);
+ assertThat(mutableDistribution.getExemplars()).isEqualTo(new Exemplar[4]);
+
+ MutableDistribution mutableDistributionNoHistogram =
+ MutableDistribution.create(BUCKET_BOUNDARIES_EMPTY);
+ assertThat(mutableDistributionNoHistogram.getExemplars()).isNull();
+ }
+
+ @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(
+ MutableSumDouble.create(),
+ MutableSumLong.create(),
+ MutableCount.create(),
+ MutableMean.create(),
+ MutableDistribution.create(BUCKET_BOUNDARIES),
+ MutableLastValueDouble.create(),
+ MutableLastValueLong.create());
+
+ 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, Collections.<String, String>emptyMap(), TIMESTAMP);
+ }
+ }
+
+ assertAggregationDataEquals(
+ aggregations.get(0).toAggregationData(),
+ AggregationData.SumDataDouble.create(20.0),
+ TOLERANCE);
+ assertAggregationDataEquals(
+ aggregations.get(1).toAggregationData(), AggregationData.SumDataLong.create(20), TOLERANCE);
+ assertAggregationDataEquals(
+ aggregations.get(2).toAggregationData(), AggregationData.CountData.create(5), TOLERANCE);
+ assertAggregationDataEquals(
+ aggregations.get(3).toAggregationData(),
+ AggregationData.MeanData.create(4.0, 5),
+ TOLERANCE);
+ assertAggregationDataEquals(
+ aggregations.get(4).toAggregationData(),
+ AggregationData.DistributionData.create(
+ 4.0, 5, -5.0, 20.0, 372, Arrays.asList(0L, 2L, 2L, 1L)),
+ TOLERANCE);
+ assertAggregationDataEquals(
+ aggregations.get(5).toAggregationData(),
+ AggregationData.LastValueDataDouble.create(5.0),
+ TOLERANCE);
+ assertAggregationDataEquals(
+ aggregations.get(6).toAggregationData(),
+ AggregationData.LastValueDataLong.create(5),
+ TOLERANCE);
+ }
+
+ @Test
+ public void testAdd_DistributionWithExemplarAttachments() {
+ MutableDistribution mutableDistribution = MutableDistribution.create(BUCKET_BOUNDARIES);
+ MutableDistribution mutableDistributionNoHistogram =
+ MutableDistribution.create(BUCKET_BOUNDARIES_EMPTY);
+ List<Double> values = Arrays.asList(-1.0, 1.0, -5.0, 20.0, 5.0);
+ List<Map<String, String>> attachmentsList =
+ ImmutableList.<Map<String, String>>of(
+ Collections.<String, String>singletonMap("k1", "v1"),
+ Collections.<String, String>singletonMap("k2", "v2"),
+ Collections.<String, String>singletonMap("k3", "v3"),
+ Collections.<String, String>singletonMap("k4", "v4"),
+ Collections.<String, String>singletonMap("k5", "v5"));
+ List<Timestamp> timestamps =
+ Arrays.asList(
+ Timestamp.fromMillis(500),
+ Timestamp.fromMillis(1000),
+ Timestamp.fromMillis(2000),
+ Timestamp.fromMillis(3000),
+ Timestamp.fromMillis(4000));
+ for (int i = 0; i < values.size(); i++) {
+ mutableDistribution.add(values.get(i), attachmentsList.get(i), timestamps.get(i));
+ mutableDistributionNoHistogram.add(values.get(i), attachmentsList.get(i), timestamps.get(i));
+ }
+
+ // Each bucket can only have up to one exemplar. If there are more than one exemplars in a
+ // bucket, only the last one will be kept.
+ List<Exemplar> expected =
+ Arrays.<Exemplar>asList(
+ null,
+ Exemplar.create(values.get(2), timestamps.get(2), attachmentsList.get(2)),
+ Exemplar.create(values.get(4), timestamps.get(4), attachmentsList.get(4)),
+ Exemplar.create(values.get(3), timestamps.get(3), attachmentsList.get(3)));
+ assertThat(mutableDistribution.getExemplars())
+ .asList()
+ .containsExactlyElementsIn(expected)
+ .inOrder();
+ assertThat(mutableDistributionNoHistogram.getExemplars()).isNull();
+ }
+
+ @Test
+ public void testCombine_SumCountMean() {
+ // combine() for Mutable Sum, Count and Mean will pick up fractional stats
+ List<MutableAggregation> aggregations1 =
+ Arrays.asList(
+ MutableSumDouble.create(),
+ MutableSumLong.create(),
+ MutableCount.create(),
+ MutableMean.create());
+ List<MutableAggregation> aggregations2 =
+ Arrays.asList(
+ MutableSumDouble.create(),
+ MutableSumLong.create(),
+ MutableCount.create(),
+ MutableMean.create());
+
+ for (double val : Arrays.asList(-1.0, -5.0)) {
+ for (MutableAggregation aggregation : aggregations1) {
+ aggregation.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+ }
+ }
+ for (double val : Arrays.asList(10.0, 50.0)) {
+ for (MutableAggregation aggregation : aggregations2) {
+ aggregation.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+ }
+ }
+
+ List<MutableAggregation> combined =
+ Arrays.asList(
+ MutableSumDouble.create(),
+ MutableSumLong.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(((MutableSumDouble) combined.get(0)).getSum()).isWithin(TOLERANCE).of(30);
+ assertThat(((MutableSumLong) combined.get(1)).getSum()).isWithin(TOLERANCE).of(30);
+ assertThat(((MutableCount) combined.get(2)).getCount()).isEqualTo(3);
+ assertThat(((MutableMean) combined.get(3)).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, Collections.<String, String>emptyMap(), TIMESTAMP);
+ }
+ for (double val : Arrays.asList(10.0, 20.0)) {
+ distribution2.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+ }
+ for (double val : Arrays.asList(-10.0, 15.0, -15.0, -20.0)) {
+ distribution3.add(val, Collections.<String, String>emptyMap(), TIMESTAMP);
+ }
+
+ 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);
+ }
+
+ @Test
+ public void mutableAggregation_ToAggregationData() {
+ assertThat(MutableSumDouble.create().toAggregationData()).isEqualTo(SumDataDouble.create(0));
+ assertThat(MutableSumLong.create().toAggregationData()).isEqualTo(SumDataLong.create(0));
+ assertThat(MutableCount.create().toAggregationData()).isEqualTo(CountData.create(0));
+ assertThat(MutableMean.create().toAggregationData()).isEqualTo(MeanData.create(0, 0));
+ assertThat(MutableDistribution.create(BUCKET_BOUNDARIES).toAggregationData())
+ .isEqualTo(
+ DistributionData.create(
+ 0,
+ 0,
+ Double.POSITIVE_INFINITY,
+ Double.NEGATIVE_INFINITY,
+ 0,
+ Arrays.asList(0L, 0L, 0L, 0L)));
+ assertThat(MutableLastValueDouble.create().toAggregationData())
+ .isEqualTo(LastValueDataDouble.create(Double.NaN));
+ assertThat(MutableLastValueLong.create().toAggregationData())
+ .isEqualTo(LastValueDataLong.create(0));
+ }
+
+ @Test
+ public void mutableAggregation_ToPoint() {
+ assertThat(MutableSumDouble.create().toPoint(TIMESTAMP))
+ .isEqualTo(Point.create(Value.doubleValue(0), TIMESTAMP));
+ assertThat(MutableSumLong.create().toPoint(TIMESTAMP))
+ .isEqualTo(Point.create(Value.longValue(0), TIMESTAMP));
+ assertThat(MutableCount.create().toPoint(TIMESTAMP))
+ .isEqualTo(Point.create(Value.longValue(0), TIMESTAMP));
+ assertThat(MutableMean.create().toPoint(TIMESTAMP))
+ .isEqualTo(Point.create(Value.doubleValue(0), TIMESTAMP));
+
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage("bucket boundary should be > 0");
+ assertThat(MutableDistribution.create(BUCKET_BOUNDARIES).toPoint(TIMESTAMP))
+ .isEqualTo(
+ Point.create(
+ Value.distributionValue(
+ Distribution.create(
+ 0,
+ 0,
+ 0,
+ BucketOptions.explicitOptions(BUCKET_BOUNDARIES.getBoundaries()),
+ Arrays.asList(
+ Bucket.create(0),
+ Bucket.create(0),
+ Bucket.create(0),
+ Bucket.create(0)))),
+ TIMESTAMP));
+ }
+
+ 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..06f50fed
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.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.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MutableViewData}. */
+@RunWith(JUnit4.class)
+public class MutableViewDataTest {
+
+ @Test
+ public void testConstants() {
+ assertThat(MutableViewData.ZERO_TIMESTAMP).isEqualTo(Timestamp.create(0, 0));
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/RecordUtilsTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/RecordUtilsTest.java
new file mode 100644
index 00000000..1e22a7a1
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/RecordUtilsTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2018, 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.ImmutableMap;
+import io.opencensus.implcore.stats.MutableAggregation.MutableDistribution;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+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.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 RecordUtils}. */
+@RunWith(JUnit4.class)
+public class RecordUtilsTest {
+
+ private static final double EPSILON = 1e-7;
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure1", "description", "1");
+ private static final MeasureLong MEASURE_LONG =
+ MeasureLong.create("measure2", "description", "1");
+ 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");
+
+ @Test
+ public void testConstants() {
+ assertThat(RecordUtils.UNKNOWN_TAG_VALUE).isNull();
+ }
+
+ @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(RecordUtils.getTagValues(tags, columns))
+ .containsExactly(CALLER_V, METHOD_V, RecordUtils.UNKNOWN_TAG_VALUE)
+ .inOrder();
+ }
+
+ @Test
+ public void createMutableAggregation() {
+ BucketBoundaries bucketBoundaries = BucketBoundaries.create(Arrays.asList(-1.0, 0.0, 1.0));
+
+ assertThat(
+ RecordUtils.createMutableAggregation(Sum.create(), MEASURE_DOUBLE).toAggregationData())
+ .isEqualTo(SumDataDouble.create(0));
+ assertThat(RecordUtils.createMutableAggregation(Sum.create(), MEASURE_LONG).toAggregationData())
+ .isEqualTo(SumDataLong.create(0));
+ assertThat(
+ RecordUtils.createMutableAggregation(Count.create(), MEASURE_DOUBLE)
+ .toAggregationData())
+ .isEqualTo(CountData.create(0));
+ assertThat(
+ RecordUtils.createMutableAggregation(Count.create(), MEASURE_LONG).toAggregationData())
+ .isEqualTo(CountData.create(0));
+ assertThat(
+ RecordUtils.createMutableAggregation(Mean.create(), MEASURE_DOUBLE).toAggregationData())
+ .isEqualTo(MeanData.create(0, 0));
+ assertThat(
+ RecordUtils.createMutableAggregation(Mean.create(), MEASURE_LONG).toAggregationData())
+ .isEqualTo(MeanData.create(0, 0));
+ assertThat(
+ RecordUtils.createMutableAggregation(LastValue.create(), MEASURE_DOUBLE)
+ .toAggregationData())
+ .isEqualTo(LastValueDataDouble.create(Double.NaN));
+ assertThat(
+ RecordUtils.createMutableAggregation(LastValue.create(), MEASURE_LONG)
+ .toAggregationData())
+ .isEqualTo(LastValueDataLong.create(0));
+
+ MutableDistribution mutableDistribution =
+ (MutableDistribution)
+ RecordUtils.createMutableAggregation(
+ Distribution.create(bucketBoundaries), MEASURE_DOUBLE);
+ assertThat(mutableDistribution.getMin()).isPositiveInfinity();
+ assertThat(mutableDistribution.getMax()).isNegativeInfinity();
+ assertThat(mutableDistribution.getSumOfSquaredDeviations()).isWithin(EPSILON).of(0);
+ assertThat(mutableDistribution.getBucketCounts()).isEqualTo(new long[4]);
+ }
+}
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..04861df9
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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
+ @SuppressWarnings("deprecation")
+ public void setState_Disabled() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ assertThat(statsComponent.getState()).isEqualTo(StatsCollectionState.DISABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void setState_Enabled() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ statsComponent.setState(StatsCollectionState.ENABLED);
+ assertThat(statsComponent.getState()).isEqualTo(StatsCollectionState.ENABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void setState_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("newState");
+ statsComponent.setState(null);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ 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..bd8b5b88
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java
@@ -0,0 +1,349 @@
+/*
+ * 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 io.grpc.Context;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.stats.StatsTestUtil.SimpleTagContext;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.DistributionData.Exemplar;
+import io.opencensus.stats.BucketBoundaries;
+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.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 MeasureDouble MEASURE_DOUBLE_NO_VIEW_1 =
+ MeasureDouble.create("my measurement no view 1", "description", "us");
+ private static final MeasureDouble MEASURE_DOUBLE_NO_VIEW_2 =
+ MeasureDouble.create("my measurement no view 2", "description", "us");
+ private static final View.Name VIEW_NAME = View.Name.create("my view");
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(-10.0, 0.0, 10.0));
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+ private static final Distribution DISTRIBUTION_NO_HISTOGRAM =
+ Distribution.create(BucketBoundaries.create(Collections.<Double>emptyList()));
+ private static final Timestamp START_TIME = Timestamp.fromMillis(0);
+ private static final Duration ONE_SECOND = Duration.fromMillis(1000);
+
+ private final TestClock testClock = TestClock.create();
+ private final StatsComponent statsComponent =
+ new StatsComponentImplBase(new SimpleEventQueue(), testClock);
+
+ 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 record_UnregisteredMeasure() {
+ View view =
+ View.create(
+ VIEW_NAME,
+ "description",
+ MEASURE_DOUBLE,
+ Sum.create(),
+ Arrays.asList(KEY),
+ Cumulative.create());
+ viewManager.registerView(view);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE_NO_VIEW_1, 1.0)
+ .put(MEASURE_DOUBLE, 2.0)
+ .put(MEASURE_DOUBLE_NO_VIEW_2, 3.0)
+ .record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+
+ // There should be one entry.
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(Sum.create(), MEASURE_DOUBLE, 2.0)),
+ 1e-6);
+ }
+
+ @Test
+ public void record_WithAttachments_Distribution() {
+ testClock.setTime(START_TIME);
+ View view =
+ View.create(VIEW_NAME, "description", MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+ viewManager.registerView(view);
+ recordWithAttachments();
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertThat(viewData).isNotNull();
+ DistributionData distributionData =
+ (DistributionData) viewData.getAggregationMap().get(Collections.singletonList(VALUE));
+ List<Exemplar> expected =
+ Arrays.asList(
+ Exemplar.create(-20.0, Timestamp.create(4, 0), Collections.singletonMap("k3", "v1")),
+ Exemplar.create(-5.0, Timestamp.create(5, 0), Collections.singletonMap("k3", "v3")),
+ Exemplar.create(1.0, Timestamp.create(2, 0), Collections.singletonMap("k2", "v2")),
+ Exemplar.create(12.0, Timestamp.create(3, 0), Collections.singletonMap("k1", "v3")));
+ assertThat(distributionData.getExemplars()).containsExactlyElementsIn(expected).inOrder();
+ }
+
+ @Test
+ public void record_WithAttachments_DistributionNoHistogram() {
+ testClock.setTime(START_TIME);
+ View view =
+ View.create(
+ VIEW_NAME,
+ "description",
+ MEASURE_DOUBLE,
+ DISTRIBUTION_NO_HISTOGRAM,
+ Arrays.asList(KEY));
+ viewManager.registerView(view);
+ recordWithAttachments();
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertThat(viewData).isNotNull();
+ DistributionData distributionData =
+ (DistributionData) viewData.getAggregationMap().get(Collections.singletonList(VALUE));
+ // Recording exemplar has no effect if there's no histogram.
+ assertThat(distributionData.getExemplars()).isEmpty();
+ }
+
+ @Test
+ public void record_WithAttachments_Count() {
+ testClock.setTime(START_TIME);
+ View view =
+ View.create(VIEW_NAME, "description", MEASURE_DOUBLE, Count.create(), Arrays.asList(KEY));
+ viewManager.registerView(view);
+ recordWithAttachments();
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertThat(viewData).isNotNull();
+ CountData countData =
+ (CountData) viewData.getAggregationMap().get(Collections.singletonList(VALUE));
+ // Recording exemplar does not affect views with an aggregation other than distribution.
+ assertThat(countData.getCount()).isEqualTo(6L);
+ }
+
+ private void recordWithAttachments() {
+ TagContext context = new SimpleTagContext(Tag.create(KEY, VALUE));
+
+ // The test Distribution has bucket boundaries [-10.0, 0.0, 10.0].
+
+ testClock.advanceTime(ONE_SECOND); // 1st second.
+ // -1.0 is in the 2nd bucket [-10.0, 0.0).
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, -1.0)
+ .putAttachment("k1", "v1")
+ .record(context);
+
+ testClock.advanceTime(ONE_SECOND); // 2nd second.
+ // 1.0 is in the 3rd bucket [0.0, 10.0).
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.0)
+ .putAttachment("k2", "v2")
+ .record(context);
+
+ testClock.advanceTime(ONE_SECOND); // 3rd second.
+ // 12.0 is in the 4th bucket [10.0, +Inf).
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 12.0)
+ .putAttachment("k1", "v3")
+ .record(context);
+
+ testClock.advanceTime(ONE_SECOND); // 4th second.
+ // -20.0 is in the 1st bucket [-Inf, -10.0).
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, -20.0)
+ .putAttachment("k3", "v1")
+ .record(context);
+
+ testClock.advanceTime(ONE_SECOND); // 5th second.
+ // -5.0 is in the 2nd bucket [-10.0, 0), should overwrite the previous exemplar -1.0.
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, -5.0)
+ .putAttachment("k3", "v3")
+ .record(context);
+
+ testClock.advanceTime(ONE_SECOND); // 6th second.
+ // -3.0 is in the 2nd bucket [-10.0, 0), but this value doesn't come with attachments, so it
+ // shouldn't overwrite the previous exemplar (-5.0).
+ statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, -3.0).record(context);
+ }
+
+ @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
+ @SuppressWarnings("deprecation")
+ 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
+ @SuppressWarnings("deprecation")
+ 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);
+ }
+}
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..ea1bf346
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java
@@ -0,0 +1,232 @@
+/*
+ * 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 com.google.common.collect.Lists;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+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.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+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.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+
+/** Stats test utilities. */
+final class StatsTestUtil {
+
+ private static final Timestamp EMPTY = Timestamp.create(0, 0);
+
+ 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 =
+ RecordUtils.createMutableAggregation(aggregation, measure);
+ for (double value : values) {
+ mutableAggregation.add(value, Collections.<String, String>emptyMap(), EMPTY);
+ }
+ return mutableAggregation.toAggregationData();
+ }
+
+ /**
+ * 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 (Entry<? extends List<? extends TagValue>, ? extends AggregationData> entry :
+ actual.entrySet()) {
+ assertAggregationDataEquals(expected.get(entry.getKey()), entry.getValue(), 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<DistributionData, Void>() {
+ @Override
+ public Void apply(DistributionData arg) {
+ assertThat(actual).isInstanceOf(DistributionData.class);
+ assertDistributionDataEquals(arg, (DistributionData) actual, tolerance);
+ return null;
+ }
+ },
+ new Function<LastValueDataDouble, Void>() {
+ @Override
+ public Void apply(LastValueDataDouble arg) {
+ assertThat(actual).isInstanceOf(LastValueDataDouble.class);
+ assertThat(((LastValueDataDouble) actual).getLastValue())
+ .isWithin(tolerance)
+ .of(arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<LastValueDataLong, Void>() {
+ @Override
+ public Void apply(LastValueDataLong arg) {
+ assertThat(actual).isInstanceOf(LastValueDataLong.class);
+ assertThat(((LastValueDataLong) actual).getLastValue()).isEqualTo(arg.getLastValue());
+ return null;
+ }
+ },
+ new Function<AggregationData, Void>() {
+ @Override
+ public Void apply(AggregationData arg) {
+ if (arg instanceof MeanData) {
+ assertThat(actual).isInstanceOf(MeanData.class);
+ assertThat(((MeanData) actual).getMean())
+ .isWithin(tolerance)
+ .of(((MeanData) arg).getMean());
+ return null;
+ }
+ throw new IllegalArgumentException("Unknown Aggregation.");
+ }
+ });
+ }
+
+ // 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()));
+ }
+
+ @Nullable
+ 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;
+ }
+
+ 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/ViewManagerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java
new file mode 100644
index 00000000..a4018b79
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java
@@ -0,0 +1,1021 @@
+/*
+ * 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.LastValue;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.LastValueDataDouble;
+import io.opencensus.stats.AggregationData.LastValueDataLong;
+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 java.util.Set;
+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 long MILLIS_PER_SECOND = 1000;
+ private static final Duration TEN_SECONDS = Duration.create(10, 0);
+ private static final Interval INTERVAL = Interval.create(TEN_SECONDS);
+
+ 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 static final LastValue LAST_VALUE = LastValue.create();
+
+ 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 testGetAllExportedViews() {
+ assertThat(viewManager.getAllExportedViews()).isEmpty();
+ View cumulativeView1 =
+ createCumulativeView(
+ View.Name.create("View 1"), MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+ View cumulativeView2 =
+ createCumulativeView(
+ View.Name.create("View 2"), MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+ View intervalView =
+ View.create(
+ View.Name.create("View 3"),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ INTERVAL);
+ viewManager.registerView(cumulativeView1);
+ viewManager.registerView(cumulativeView2);
+ viewManager.registerView(intervalView);
+
+ // Only cumulative views should be exported.
+ assertThat(viewManager.getAllExportedViews()).containsExactly(cumulativeView1, cumulativeView2);
+ }
+
+ @Test
+ public void getAllExportedViewsResultIsUnmodifiable() {
+ View view1 =
+ View.create(
+ View.Name.create("View 1"),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ viewManager.registerView(view1);
+ Set<View> exported = viewManager.getAllExportedViews();
+
+ View view2 =
+ View.create(
+ View.Name.create("View 2"),
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ thrown.expect(UnsupportedOperationException.class);
+ exported.add(view2);
+ }
+
+ @Test
+ public void testRegisterAndGetIntervalView() {
+ View intervalView =
+ View.create(
+ VIEW_NAME,
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ INTERVAL);
+ 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);
+ }
+
+ @Test
+ public void testRecordDouble_lastvalue_cumulative() {
+ testRecordCumulative(MEASURE_DOUBLE, LAST_VALUE, 11.1, 22.2, 33.3, 44.4);
+ }
+
+ @Test
+ public void testRecordLong_lastvalue_cumulative() {
+ testRecordCumulative(MEASURE_LONG, LAST_VALUE, 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[] {10, 24, 30, 40, 50},
+ -50,
+ 30,
+ SumDataLong.create(Math.round(34 * 0.6 + 120)),
+ SumDataLong.create(-40),
+ SumDataLong.create(30));
+ }
+
+ @Test
+ public void testRecordDouble_lastvalue_interval() {
+ testRecordInterval(
+ MEASURE_DOUBLE,
+ LAST_VALUE,
+ new double[] {20.0, -1.0, 1.0, -5.0, 5.0},
+ 9.0,
+ 30.0,
+ LastValueDataDouble.create(5.0),
+ LastValueDataDouble.create(9.0),
+ LastValueDataDouble.create(30.0));
+ }
+
+ @Test
+ public void testRecordLong_lastvalue_interval() {
+ testRecordInterval(
+ MEASURE_LONG,
+ LAST_VALUE,
+ new double[] {1000, 2000, 3000, 4000, 5000},
+ -5000,
+ 30,
+ LastValueDataLong.create(5000),
+ LastValueDataLong.create(-5000),
+ LastValueDataLong.create(30));
+ }
+
+ private final 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(RecordUtils.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(RecordUtils.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
+ @SuppressWarnings("deprecation")
+ 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
+ @SuppressWarnings("deprecation")
+ 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
+ @SuppressWarnings("deprecation")
+ 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
+ @SuppressWarnings("deprecation")
+ 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);
+ }
+
+ @SuppressWarnings("deprecation")
+ 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/ScopedTagContextsTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.java
new file mode 100644
index 00000000..6a8fe4c7
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.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 io.opencensus.common.Scope;
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+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 CurrentState(State.ENABLED));
+
+ @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..1859e081
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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 org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import io.opencensus.implcore.internal.CurrentState;
+import io.opencensus.implcore.internal.CurrentState.State;
+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 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}.
+ *
+ * <p>Tests for {@link TagContextBuilderImpl#buildScoped()} are in {@link ScopedTagContextsTest}.
+ */
+@RunWith(JUnit4.class)
+public class TagContextImplTest {
+ private final Tagger tagger = new TaggerImpl(new CurrentState(State.ENABLED));
+
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = 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 getTags_empty() {
+ TagContextImpl tags = new TagContextImpl(ImmutableMap.<TagKey, TagValue>of());
+ assertThat(tags.getTags()).isEmpty();
+ }
+
+ @Test
+ public void getTags_nonEmpty() {
+ TagContextImpl tags = new TagContextImpl(ImmutableMap.of(K1, V1, K2, V2));
+ assertThat(tags.getTags()).containsExactly(K1, V1, K2, V2);
+ }
+
+ @Test
+ public void put_newKey() {
+ TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+ assertThat(((TagContextImpl) tagger.toBuilder(tags).put(K2, V2).build()).getTags())
+ .containsExactly(K1, V1, K2, V2);
+ }
+
+ @Test
+ public void put_existingKey() {
+ TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+ assertThat(((TagContextImpl) tagger.toBuilder(tags).put(K1, V2).build()).getTags())
+ .containsExactly(K1, V2);
+ }
+
+ @Test
+ public void put_nullKey() {
+ TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+ TagContextBuilder builder = tagger.toBuilder(tags);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("key");
+ builder.put(null, V2);
+ }
+
+ @Test
+ public void put_nullValue() {
+ TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+ TagContextBuilder builder = tagger.toBuilder(tags);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("value");
+ builder.put(K2, null);
+ }
+
+ @Test
+ public void remove_existingKey() {
+ TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1, K2, V2));
+ assertThat(((TagContextImpl) tagger.toBuilder(tags).remove(K1).build()).getTags())
+ .containsExactly(K2, V2);
+ }
+
+ @Test
+ public void remove_differentKey() {
+ TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+ assertThat(((TagContextImpl) tagger.toBuilder(tags).remove(K2).build()).getTags())
+ .containsExactly(K1, V1);
+ }
+
+ @Test
+ public void remove_nullKey() {
+ TagContext tags = new TagContextImpl(ImmutableMap.of(K1, V1));
+ TagContextBuilder builder = tagger.toBuilder(tags);
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("key");
+ builder.remove(null);
+ }
+
+ @Test
+ public void testIterator() {
+ TagContextImpl tags = new TagContextImpl(ImmutableMap.of(K1, V1, K2, V2));
+ Iterator<Tag> i = tags.getIterator();
+ 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(K1, V1), Tag.create(K2, V2));
+ thrown.expect(NoSuchElementException.class);
+ i.next();
+ }
+
+ @Test
+ public void disallowCallingRemoveOnIterator() {
+ TagContextImpl tags = new TagContextImpl(ImmutableMap.of(K1, V1, K2, V2));
+ Iterator<Tag> i = tags.getIterator();
+ i.next();
+ thrown.expect(UnsupportedOperationException.class);
+ i.remove();
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ tagger.emptyBuilder().put(K1, V1).put(K2, V2).build(),
+ tagger.emptyBuilder().put(K1, V1).put(K2, V2).build(),
+ tagger.emptyBuilder().put(K2, V2).put(K1, V1).build(),
+ new TagContext() {
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return Lists.<Tag>newArrayList(Tag.create(K1, V1), Tag.create(K2, V2)).iterator();
+ }
+ })
+ .addEqualityGroup(tagger.emptyBuilder().put(K1, V1).put(K2, V1).build())
+ .addEqualityGroup(tagger.emptyBuilder().put(K1, V2).put(K2, 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..1bc13c59
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsComponentImplBaseTest.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 static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+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 TagsComponentImplBase}. */
+@RunWith(JUnit4.class)
+public class TagsComponentImplBaseTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private final TagsComponent tagsComponent = new TagsComponentImplBase();
+
+ @Test
+ public void defaultState() {
+ assertThat(tagsComponent.getState()).isEqualTo(TaggingState.ENABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void setState_Disabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagsComponent.getState()).isEqualTo(TaggingState.DISABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void setState_Enabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ tagsComponent.setState(TaggingState.ENABLED);
+ assertThat(tagsComponent.getState()).isEqualTo(TaggingState.ENABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void setState_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("newState");
+ tagsComponent.setState(null);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void preventSettingStateAfterGettingState_DifferentState() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ tagsComponent.getState();
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ tagsComponent.setState(TaggingState.ENABLED);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void preventSettingStateAfterGettingState_SameState() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ tagsComponent.getState();
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ tagsComponent.setState(TaggingState.DISABLED);
+ }
+}
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..dcfba508
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.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 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 {
+ private 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..26a072f6
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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
+ @SuppressWarnings("deprecation")
+ public void toByteArray_TaggingDisabled() throws TagContextSerializationException {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(serializer.toByteArray(tagContext)).isEmpty();
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ 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
+ @SuppressWarnings("deprecation")
+ 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..8db0e389
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java
@@ -0,0 +1,329 @@
+/*
+ * 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 testConstants() {
+ // 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);
+ assertThat(SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT).isEqualTo(8192);
+ }
+
+ @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 testDeserializeTooLargeByteArrayThrowException()
+ throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ for (int i = 0; i < SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT / 8 - 1; i++) {
+ // Each tag will be with format {key : "0123", value : "0123"}, so the length of it is 8.
+ String str;
+ if (i < 10) {
+ str = "000" + i;
+ } else if (i < 100) {
+ str = "00" + i;
+ } else if (i < 1000) {
+ str = "0" + i;
+ } else {
+ str = String.valueOf(i);
+ }
+ encodeTagToOutput(str, str, output);
+ }
+ // The last tag will be of size 9, so the total size of the TagContext (8193) will be one byte
+ // more than limit.
+ encodeTagToOutput("last", "last1", output);
+
+ byte[] bytes = output.toByteArray();
+ thrown.expect(TagContextDeserializationException.class);
+ thrown.expectMessage("Size of TagContext exceeds the maximum serialized size ");
+ serializer.fromByteArray(bytes);
+ }
+
+ // Deserializing this input should cause an error, even though it represents a relatively small
+ // TagContext.
+ @Test
+ public void testDeserializeTooLargeByteArrayThrowException_WithDuplicateTagKeys()
+ throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ for (int i = 0; i < SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT / 8 - 1; i++) {
+ // Each tag will be with format {key : "key_", value : "0123"}, so the length of it is 8.
+ String str;
+ if (i < 10) {
+ str = "000" + i;
+ } else if (i < 100) {
+ str = "00" + i;
+ } else if (i < 1000) {
+ str = "0" + i;
+ } else {
+ str = String.valueOf(i);
+ }
+ encodeTagToOutput("key_", str, output);
+ }
+ // The last tag will be of size 9, so the total size of the TagContext (8193) will be one byte
+ // more than limit.
+ encodeTagToOutput("key_", "last1", output);
+
+ byte[] bytes = output.toByteArray();
+ thrown.expect(TagContextDeserializationException.class);
+ thrown.expectMessage("Size of TagContext exceeds the maximum serialized size ");
+ serializer.fromByteArray(bytes);
+ }
+
+ @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 testDeserializeDuplicateKeys() throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ encodeTagToOutput("Key1", "Value1", output);
+ encodeTagToOutput("Key1", "Value2", output);
+ TagContext expected =
+ tagger.emptyBuilder().put(TagKey.create("Key1"), TagValue.create("Value2")).build();
+ assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+ }
+
+ @Test
+ public void testDeserializeNonConsecutiveDuplicateKeys()
+ throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ encodeTagToOutput("Key1", "Value1", output);
+ encodeTagToOutput("Key2", "Value2", output);
+ encodeTagToOutput("Key3", "Value3", output);
+ encodeTagToOutput("Key1", "Value4", output);
+ encodeTagToOutput("Key2", "Value5", output);
+ TagContext expected =
+ tagger
+ .emptyBuilder()
+ .put(TagKey.create("Key1"), TagValue.create("Value4"))
+ .put(TagKey.create("Key2"), TagValue.create("Value5"))
+ .put(TagKey.create("Key3"), TagValue.create("Value3"))
+ .build();
+ assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+ }
+
+ @Test
+ public void testDeserializeDuplicateTags() throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ encodeTagToOutput("Key1", "Value1", output);
+ encodeTagToOutput("Key1", "Value1", output);
+ TagContext expected =
+ tagger.emptyBuilder().put(TagKey.create("Key1"), TagValue.create("Value1")).build();
+ assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+ }
+
+ @Test
+ public void testDeserializeNonConsecutiveDuplicateTags()
+ throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ encodeTagToOutput("Key1", "Value1", output);
+ encodeTagToOutput("Key2", "Value2", output);
+ encodeTagToOutput("Key3", "Value3", output);
+ 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"))
+ .put(TagKey.create("Key3"), TagValue.create("Value3"))
+ .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 supports version up to: 0");
+ serializer.fromByteArray(new byte[] {(byte) (SerializationUtils.VERSION_ID + 1)});
+ }
+
+ @Test
+ public void testDeserializeNegativeVersionId() throws TagContextDeserializationException {
+ thrown.expect(TagContextDeserializationException.class);
+ thrown.expectMessage("Wrong Version ID: -1. Currently supports version up to: 0");
+ serializer.fromByteArray(new byte[] {(byte) -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..1b1aa042
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.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 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_NormalTagContext() 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());
+ }
+
+ @Test
+ public void testRoundtrip_TagContextWithMaximumSize() throws Exception {
+ TagContextBuilder builder = tagger.emptyBuilder();
+ for (int i = 0; i < SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT / 8; i++) {
+ // Each tag will be with format {key : "0123", value : "0123"}, so the length of it is 8.
+ // Add 1024 tags, the total size should just be 8192.
+ String str;
+ if (i < 10) {
+ str = "000" + i;
+ } else if (i < 100) {
+ str = "00" + i;
+ } else if (i < 1000) {
+ str = "0" + i;
+ } else {
+ str = "" + i;
+ }
+ builder.put(TagKey.create(str), TagValue.create(str));
+ }
+ testRoundtripSerialization(builder.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..ed68fe3d
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.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.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.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.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.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+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 {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ 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);
+ }
+
+ @Test
+ public void testSerializeTooLargeTagContext() throws TagContextSerializationException {
+ TagContextBuilder builder = tagger.emptyBuilder();
+ for (int i = 0; i < SerializationUtils.TAGCONTEXT_SERIALIZED_SIZE_LIMIT / 8 - 1; i++) {
+ // Each tag will be with format {key : "0123", value : "0123"}, so the length of it is 8.
+ String str;
+ if (i < 10) {
+ str = "000" + i;
+ } else if (i < 100) {
+ str = "00" + i;
+ } else if (i < 1000) {
+ str = "0" + i;
+ } else {
+ str = String.valueOf(i);
+ }
+ builder.put(TagKey.create(str), TagValue.create(str));
+ }
+ // The last tag will be of size 9, so the total size of the TagContext (8193) will be one byte
+ // more than limit.
+ builder.put(TagKey.create("last"), TagValue.create("last1"));
+
+ TagContext tagContext = builder.build();
+ thrown.expect(TagContextSerializationException.class);
+ thrown.expectMessage("Size of TagContext exceeds the maximum serialized size ");
+ serializer.toByteArray(tagContext);
+ }
+
+ 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(SerializationUtils.VERSION_ID);
+ for (Tag tag : list) {
+ expected.write(SerializationUtils.TAG_FIELD_ID);
+ encodeString(tag.getKey().getName(), expected);
+ encodeString(tag.getValue().asString(), expected);
+ }
+ possibleOutputs.add(new String(expected.toByteArray(), Charsets.UTF_8));
+ }
+
+ 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));
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/NoRecordEventsSpanImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/NoRecordEventsSpanImplTest.java
new file mode 100644
index 00000000..c576860d
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/NoRecordEventsSpanImplTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2018, 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.MessageEvent;
+import io.opencensus.trace.NetworkEvent;
+import io.opencensus.trace.Span.Options;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.Tracestate;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link NoRecordEventsSpanImpl}. */
+@RunWith(JUnit4.class)
+public class NoRecordEventsSpanImplTest {
+ private final Random random = new Random(1234);
+ private final SpanContext spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ TraceOptions.DEFAULT,
+ Tracestate.builder().build());
+ private final NoRecordEventsSpanImpl noRecordEventsSpan =
+ NoRecordEventsSpanImpl.create(spanContext);
+
+ @Test
+ public void propagatesSpanContext() {
+ assertThat(noRecordEventsSpan.getContext()).isEqualTo(spanContext);
+ }
+
+ @Test
+ public void hasNoRecordEventsOption() {
+ assertThat(noRecordEventsSpan.getOptions()).doesNotContain(Options.RECORD_EVENTS);
+ }
+
+ @Test
+ public void doNotCrash() {
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ attributes.put(
+ "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ Map<String, AttributeValue> multipleAttributes = new HashMap<String, AttributeValue>();
+ multipleAttributes.put(
+ "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ multipleAttributes.put("MyBooleanAttributeKey", AttributeValue.booleanAttributeValue(true));
+ multipleAttributes.put("MyLongAttributeKey", AttributeValue.longAttributeValue(123));
+ // Tests only that all the methods are not crashing/throwing errors.
+ noRecordEventsSpan.putAttribute(
+ "MyStringAttributeKey2", AttributeValue.stringAttributeValue("MyStringAttributeValue2"));
+ noRecordEventsSpan.addAttributes(attributes);
+ noRecordEventsSpan.addAttributes(multipleAttributes);
+ noRecordEventsSpan.addAnnotation("MyAnnotation");
+ noRecordEventsSpan.addAnnotation("MyAnnotation", attributes);
+ noRecordEventsSpan.addAnnotation("MyAnnotation", multipleAttributes);
+ noRecordEventsSpan.addAnnotation(Annotation.fromDescription("MyAnnotation"));
+ noRecordEventsSpan.addNetworkEvent(NetworkEvent.builder(NetworkEvent.Type.SENT, 1L).build());
+ noRecordEventsSpan.addMessageEvent(MessageEvent.builder(MessageEvent.Type.SENT, 1L).build());
+ noRecordEventsSpan.addLink(
+ Link.fromSpanContext(SpanContext.INVALID, Link.Type.CHILD_LINKED_SPAN));
+ noRecordEventsSpan.setStatus(Status.OK);
+ noRecordEventsSpan.end(EndSpanOptions.DEFAULT);
+ noRecordEventsSpan.end();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/RecordEventsSpanImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/RecordEventsSpanImplTest.java
new file mode 100644
index 00000000..b293a225
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/RecordEventsSpanImplTest.java
@@ -0,0 +1,594 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.Annotation;
+import io.opencensus.trace.AttributeValue;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Link;
+import io.opencensus.trace.NetworkEvent;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link RecordEventsSpanImpl}. */
+@RunWith(JUnit4.class)
+public class RecordEventsSpanImplTest {
+ private static final String SPAN_NAME = "MySpanName";
+ private static final String ANNOTATION_DESCRIPTION = "MyAnnotation";
+ private final Random random = new Random(1234);
+ private final SpanContext spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+ private final SpanId parentSpanId = SpanId.generateRandomId(random);
+ private final Timestamp timestamp = Timestamp.create(1234, 5678);
+ private final TestClock testClock = TestClock.create(timestamp);
+ private final TimestampConverter timestampConverter = TimestampConverter.now(testClock);
+ private final Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ private final Map<String, AttributeValue> expectedAttributes =
+ new HashMap<String, AttributeValue>();
+ @Mock private StartEndHandler startEndHandler;
+ @Rule public final ExpectedException exception = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ attributes.put(
+ "MyStringAttributeKey", AttributeValue.stringAttributeValue("MyStringAttributeValue"));
+ attributes.put("MyLongAttributeKey", AttributeValue.longAttributeValue(123L));
+ attributes.put("MyBooleanAttributeKey", AttributeValue.booleanAttributeValue(false));
+ expectedAttributes.putAll(attributes);
+ expectedAttributes.put(
+ "MySingleStringAttributeKey",
+ AttributeValue.stringAttributeValue("MySingleStringAttributeValue"));
+ }
+
+ @Test
+ public void noEventsRecordedAfterEnd() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ span.end();
+ // Check that adding trace events after Span#end() does not throw any exception and are not
+ // recorded.
+ span.putAttributes(attributes);
+ span.putAttribute(
+ "MySingleStringAttributeKey",
+ AttributeValue.stringAttributeValue("MySingleStringAttributeValue"));
+ span.addAnnotation(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+ span.addAnnotation(ANNOTATION_DESCRIPTION, attributes);
+ span.addNetworkEvent(
+ NetworkEvent.builder(NetworkEvent.Type.RECV, 1).setUncompressedMessageSize(3).build());
+ span.addLink(Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN));
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getStartTimestamp()).isEqualTo(timestamp);
+ assertThat(spanData.getAttributes().getAttributeMap()).isEmpty();
+ assertThat(spanData.getAnnotations().getEvents()).isEmpty();
+ assertThat(spanData.getNetworkEvents().getEvents()).isEmpty();
+ assertThat(spanData.getLinks().getLinks()).isEmpty();
+ assertThat(spanData.getStatus()).isEqualTo(Status.OK);
+ assertThat(spanData.getEndTimestamp()).isEqualTo(timestamp);
+ }
+
+ @Test
+ public void deprecatedAddAttributesStillWorks() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ span.addAttributes(attributes);
+ span.end();
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getAttributes().getAttributeMap()).isEqualTo(attributes);
+ }
+
+ @Test
+ public void toSpanData_ActiveSpan() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ true,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ Mockito.verify(startEndHandler, Mockito.times(1)).onStart(span);
+ span.putAttribute(
+ "MySingleStringAttributeKey",
+ AttributeValue.stringAttributeValue("MySingleStringAttributeValue"));
+ span.putAttributes(attributes);
+ testClock.advanceTime(Duration.create(0, 100));
+ span.addAnnotation(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+ testClock.advanceTime(Duration.create(0, 100));
+ span.addAnnotation(ANNOTATION_DESCRIPTION, attributes);
+ testClock.advanceTime(Duration.create(0, 100));
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.RECV, 1).setUncompressedMessageSize(3).build();
+ span.addNetworkEvent(networkEvent);
+ testClock.advanceTime(Duration.create(0, 100));
+ Link link = Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN);
+ span.addLink(link);
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getContext()).isEqualTo(spanContext);
+ assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+ assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+ assertThat(spanData.getHasRemoteParent()).isTrue();
+ assertThat(spanData.getAttributes().getDroppedAttributesCount()).isEqualTo(0);
+ assertThat(spanData.getAttributes().getAttributeMap()).isEqualTo(expectedAttributes);
+ assertThat(spanData.getAnnotations().getDroppedEventsCount()).isEqualTo(0);
+ assertThat(spanData.getAnnotations().getEvents().size()).isEqualTo(2);
+ assertThat(spanData.getAnnotations().getEvents().get(0).getTimestamp())
+ .isEqualTo(timestamp.addNanos(100));
+ assertThat(spanData.getAnnotations().getEvents().get(0).getEvent())
+ .isEqualTo(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+ assertThat(spanData.getAnnotations().getEvents().get(1).getTimestamp())
+ .isEqualTo(timestamp.addNanos(200));
+ assertThat(spanData.getAnnotations().getEvents().get(1).getEvent())
+ .isEqualTo(Annotation.fromDescriptionAndAttributes(ANNOTATION_DESCRIPTION, attributes));
+ assertThat(spanData.getNetworkEvents().getDroppedEventsCount()).isEqualTo(0);
+ assertThat(spanData.getNetworkEvents().getEvents().size()).isEqualTo(1);
+ assertThat(spanData.getNetworkEvents().getEvents().get(0).getTimestamp())
+ .isEqualTo(timestamp.addNanos(300));
+ assertThat(spanData.getNetworkEvents().getEvents().get(0).getEvent()).isEqualTo(networkEvent);
+ assertThat(spanData.getLinks().getDroppedLinksCount()).isEqualTo(0);
+ assertThat(spanData.getLinks().getLinks().size()).isEqualTo(1);
+ assertThat(spanData.getLinks().getLinks().get(0)).isEqualTo(link);
+ assertThat(spanData.getStartTimestamp()).isEqualTo(timestamp);
+ assertThat(spanData.getStatus()).isNull();
+ assertThat(spanData.getEndTimestamp()).isNull();
+ }
+
+ @Test
+ public void toSpanData_EndedSpan() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ Mockito.verify(startEndHandler, Mockito.times(1)).onStart(span);
+ span.putAttribute(
+ "MySingleStringAttributeKey",
+ AttributeValue.stringAttributeValue("MySingleStringAttributeValue"));
+ span.putAttributes(attributes);
+ testClock.advanceTime(Duration.create(0, 100));
+ span.addAnnotation(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+ testClock.advanceTime(Duration.create(0, 100));
+ span.addAnnotation(ANNOTATION_DESCRIPTION, attributes);
+ testClock.advanceTime(Duration.create(0, 100));
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.RECV, 1).setUncompressedMessageSize(3).build();
+ span.addNetworkEvent(networkEvent);
+ Link link = Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN);
+ span.addLink(link);
+ testClock.advanceTime(Duration.create(0, 100));
+ span.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+ Mockito.verify(startEndHandler, Mockito.times(1)).onEnd(span);
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getContext()).isEqualTo(spanContext);
+ assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+ assertThat(spanData.getParentSpanId()).isEqualTo(parentSpanId);
+ assertThat(spanData.getHasRemoteParent()).isFalse();
+ assertThat(spanData.getAttributes().getDroppedAttributesCount()).isEqualTo(0);
+ assertThat(spanData.getAttributes().getAttributeMap()).isEqualTo(expectedAttributes);
+ assertThat(spanData.getAnnotations().getDroppedEventsCount()).isEqualTo(0);
+ assertThat(spanData.getAnnotations().getEvents().size()).isEqualTo(2);
+ assertThat(spanData.getAnnotations().getEvents().get(0).getTimestamp())
+ .isEqualTo(timestamp.addNanos(100));
+ assertThat(spanData.getAnnotations().getEvents().get(0).getEvent())
+ .isEqualTo(Annotation.fromDescription(ANNOTATION_DESCRIPTION));
+ assertThat(spanData.getAnnotations().getEvents().get(1).getTimestamp())
+ .isEqualTo(timestamp.addNanos(200));
+ assertThat(spanData.getAnnotations().getEvents().get(1).getEvent())
+ .isEqualTo(Annotation.fromDescriptionAndAttributes(ANNOTATION_DESCRIPTION, attributes));
+ assertThat(spanData.getNetworkEvents().getDroppedEventsCount()).isEqualTo(0);
+ assertThat(spanData.getNetworkEvents().getEvents().size()).isEqualTo(1);
+ assertThat(spanData.getNetworkEvents().getEvents().get(0).getTimestamp())
+ .isEqualTo(timestamp.addNanos(300));
+ assertThat(spanData.getNetworkEvents().getEvents().get(0).getEvent()).isEqualTo(networkEvent);
+ assertThat(spanData.getLinks().getDroppedLinksCount()).isEqualTo(0);
+ assertThat(spanData.getLinks().getLinks().size()).isEqualTo(1);
+ assertThat(spanData.getLinks().getLinks().get(0)).isEqualTo(link);
+ assertThat(spanData.getStartTimestamp()).isEqualTo(timestamp);
+ assertThat(spanData.getStatus()).isEqualTo(Status.CANCELLED);
+ assertThat(spanData.getEndTimestamp()).isEqualTo(timestamp.addNanos(400));
+ }
+
+ @Test
+ public void status_ViaSetStatus() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ Mockito.verify(startEndHandler, Mockito.times(1)).onStart(span);
+ testClock.advanceTime(Duration.create(0, 100));
+ assertThat(span.getStatus()).isEqualTo(Status.OK);
+ span.setStatus(Status.CANCELLED);
+ assertThat(span.getStatus()).isEqualTo(Status.CANCELLED);
+ span.end();
+ assertThat(span.getStatus()).isEqualTo(Status.CANCELLED);
+ }
+
+ @Test
+ public void status_ViaEndSpanOptions() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ Mockito.verify(startEndHandler, Mockito.times(1)).onStart(span);
+ testClock.advanceTime(Duration.create(0, 100));
+ assertThat(span.getStatus()).isEqualTo(Status.OK);
+ span.setStatus(Status.CANCELLED);
+ assertThat(span.getStatus()).isEqualTo(Status.CANCELLED);
+ span.end(EndSpanOptions.builder().setStatus(Status.ABORTED).build());
+ assertThat(span.getStatus()).isEqualTo(Status.ABORTED);
+ }
+
+ @Test
+ public void droppingAttributes() {
+ final int maxNumberOfAttributes = 8;
+ TraceParams traceParams =
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfAttributes(maxNumberOfAttributes).build();
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ traceParams,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ for (int i = 0; i < 2 * maxNumberOfAttributes; i++) {
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ attributes.put("MyStringAttributeKey" + i, AttributeValue.longAttributeValue(i));
+ span.putAttributes(attributes);
+ }
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getAttributes().getDroppedAttributesCount())
+ .isEqualTo(maxNumberOfAttributes);
+ assertThat(spanData.getAttributes().getAttributeMap().size()).isEqualTo(maxNumberOfAttributes);
+ for (int i = 0; i < maxNumberOfAttributes; i++) {
+ assertThat(
+ spanData
+ .getAttributes()
+ .getAttributeMap()
+ .get("MyStringAttributeKey" + (i + maxNumberOfAttributes)))
+ .isEqualTo(AttributeValue.longAttributeValue(i + maxNumberOfAttributes));
+ }
+ span.end();
+ spanData = span.toSpanData();
+ assertThat(spanData.getAttributes().getDroppedAttributesCount())
+ .isEqualTo(maxNumberOfAttributes);
+ assertThat(spanData.getAttributes().getAttributeMap().size()).isEqualTo(maxNumberOfAttributes);
+ for (int i = 0; i < maxNumberOfAttributes; i++) {
+ assertThat(
+ spanData
+ .getAttributes()
+ .getAttributeMap()
+ .get("MyStringAttributeKey" + (i + maxNumberOfAttributes)))
+ .isEqualTo(AttributeValue.longAttributeValue(i + maxNumberOfAttributes));
+ }
+ }
+
+ @Test
+ public void droppingAndAddingAttributes() {
+ final int maxNumberOfAttributes = 8;
+ TraceParams traceParams =
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfAttributes(maxNumberOfAttributes).build();
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ traceParams,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ for (int i = 0; i < 2 * maxNumberOfAttributes; i++) {
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ attributes.put("MyStringAttributeKey" + i, AttributeValue.longAttributeValue(i));
+ span.putAttributes(attributes);
+ }
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getAttributes().getDroppedAttributesCount())
+ .isEqualTo(maxNumberOfAttributes);
+ assertThat(spanData.getAttributes().getAttributeMap().size()).isEqualTo(maxNumberOfAttributes);
+ for (int i = 0; i < maxNumberOfAttributes; i++) {
+ assertThat(
+ spanData
+ .getAttributes()
+ .getAttributeMap()
+ .get("MyStringAttributeKey" + (i + maxNumberOfAttributes)))
+ .isEqualTo(AttributeValue.longAttributeValue(i + maxNumberOfAttributes));
+ }
+ for (int i = 0; i < maxNumberOfAttributes / 2; i++) {
+ Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>();
+ attributes.put("MyStringAttributeKey" + i, AttributeValue.longAttributeValue(i));
+ span.putAttributes(attributes);
+ }
+ spanData = span.toSpanData();
+ assertThat(spanData.getAttributes().getDroppedAttributesCount())
+ .isEqualTo(maxNumberOfAttributes * 3 / 2);
+ assertThat(spanData.getAttributes().getAttributeMap().size()).isEqualTo(maxNumberOfAttributes);
+ // Test that we still have in the attributes map the latest maxNumberOfAttributes / 2 entries.
+ for (int i = 0; i < maxNumberOfAttributes / 2; i++) {
+ assertThat(
+ spanData
+ .getAttributes()
+ .getAttributeMap()
+ .get("MyStringAttributeKey" + (i + maxNumberOfAttributes * 3 / 2)))
+ .isEqualTo(AttributeValue.longAttributeValue(i + maxNumberOfAttributes * 3 / 2));
+ }
+ // Test that we have the newest re-added initial entries.
+ for (int i = 0; i < maxNumberOfAttributes / 2; i++) {
+ assertThat(spanData.getAttributes().getAttributeMap().get("MyStringAttributeKey" + i))
+ .isEqualTo(AttributeValue.longAttributeValue(i));
+ }
+ }
+
+ @Test
+ public void droppingAnnotations() {
+ final int maxNumberOfAnnotations = 8;
+ TraceParams traceParams =
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfAnnotations(maxNumberOfAnnotations).build();
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ traceParams,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ Annotation annotation = Annotation.fromDescription(ANNOTATION_DESCRIPTION);
+ for (int i = 0; i < 2 * maxNumberOfAnnotations; i++) {
+ span.addAnnotation(annotation);
+ testClock.advanceTime(Duration.create(0, 100));
+ }
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getAnnotations().getDroppedEventsCount()).isEqualTo(maxNumberOfAnnotations);
+ assertThat(spanData.getAnnotations().getEvents().size()).isEqualTo(maxNumberOfAnnotations);
+ for (int i = 0; i < maxNumberOfAnnotations; i++) {
+ assertThat(spanData.getAnnotations().getEvents().get(i).getTimestamp())
+ .isEqualTo(timestamp.addNanos(100L * (maxNumberOfAnnotations + i)));
+ assertThat(spanData.getAnnotations().getEvents().get(i).getEvent()).isEqualTo(annotation);
+ }
+ span.end();
+ spanData = span.toSpanData();
+ assertThat(spanData.getAnnotations().getDroppedEventsCount()).isEqualTo(maxNumberOfAnnotations);
+ assertThat(spanData.getAnnotations().getEvents().size()).isEqualTo(maxNumberOfAnnotations);
+ for (int i = 0; i < maxNumberOfAnnotations; i++) {
+ assertThat(spanData.getAnnotations().getEvents().get(i).getTimestamp())
+ .isEqualTo(timestamp.addNanos(100L * (maxNumberOfAnnotations + i)));
+ assertThat(spanData.getAnnotations().getEvents().get(i).getEvent()).isEqualTo(annotation);
+ }
+ }
+
+ @Test
+ public void droppingNetworkEvents() {
+ final int maxNumberOfNetworkEvents = 8;
+ TraceParams traceParams =
+ TraceParams.DEFAULT
+ .toBuilder()
+ .setMaxNumberOfNetworkEvents(maxNumberOfNetworkEvents)
+ .build();
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ traceParams,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ NetworkEvent networkEvent =
+ NetworkEvent.builder(NetworkEvent.Type.RECV, 1).setUncompressedMessageSize(3).build();
+ for (int i = 0; i < 2 * maxNumberOfNetworkEvents; i++) {
+ span.addNetworkEvent(networkEvent);
+ testClock.advanceTime(Duration.create(0, 100));
+ }
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getNetworkEvents().getDroppedEventsCount())
+ .isEqualTo(maxNumberOfNetworkEvents);
+ assertThat(spanData.getNetworkEvents().getEvents().size()).isEqualTo(maxNumberOfNetworkEvents);
+ for (int i = 0; i < maxNumberOfNetworkEvents; i++) {
+ assertThat(spanData.getNetworkEvents().getEvents().get(i).getTimestamp())
+ .isEqualTo(timestamp.addNanos(100L * (maxNumberOfNetworkEvents + i)));
+ assertThat(spanData.getNetworkEvents().getEvents().get(i).getEvent()).isEqualTo(networkEvent);
+ }
+ span.end();
+ spanData = span.toSpanData();
+ assertThat(spanData.getNetworkEvents().getDroppedEventsCount())
+ .isEqualTo(maxNumberOfNetworkEvents);
+ assertThat(spanData.getNetworkEvents().getEvents().size()).isEqualTo(maxNumberOfNetworkEvents);
+ for (int i = 0; i < maxNumberOfNetworkEvents; i++) {
+ assertThat(spanData.getNetworkEvents().getEvents().get(i).getTimestamp())
+ .isEqualTo(timestamp.addNanos(100L * (maxNumberOfNetworkEvents + i)));
+ assertThat(spanData.getNetworkEvents().getEvents().get(i).getEvent()).isEqualTo(networkEvent);
+ }
+ }
+
+ @Test
+ public void droppingLinks() {
+ final int maxNumberOfLinks = 8;
+ TraceParams traceParams =
+ TraceParams.DEFAULT.toBuilder().setMaxNumberOfLinks(maxNumberOfLinks).build();
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ traceParams,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ Link link = Link.fromSpanContext(spanContext, Link.Type.CHILD_LINKED_SPAN);
+ for (int i = 0; i < 2 * maxNumberOfLinks; i++) {
+ span.addLink(link);
+ }
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getLinks().getDroppedLinksCount()).isEqualTo(maxNumberOfLinks);
+ assertThat(spanData.getLinks().getLinks().size()).isEqualTo(maxNumberOfLinks);
+ for (int i = 0; i < maxNumberOfLinks; i++) {
+ assertThat(spanData.getLinks().getLinks().get(i)).isEqualTo(link);
+ }
+ span.end();
+ spanData = span.toSpanData();
+ assertThat(spanData.getLinks().getDroppedLinksCount()).isEqualTo(maxNumberOfLinks);
+ assertThat(spanData.getLinks().getLinks().size()).isEqualTo(maxNumberOfLinks);
+ for (int i = 0; i < maxNumberOfLinks; i++) {
+ assertThat(spanData.getLinks().getLinks().get(i)).isEqualTo(link);
+ }
+ }
+
+ @Test
+ public void sampleToLocalSpanStore() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ span.end(EndSpanOptions.builder().setSampleToLocalSpanStore(true).build());
+ Mockito.verify(startEndHandler, Mockito.times(1)).onEnd(span);
+ assertThat(span.getSampleToLocalSpanStore()).isTrue();
+ span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ span.end();
+ Mockito.verify(startEndHandler, Mockito.times(1)).onEnd(span);
+ assertThat(span.getSampleToLocalSpanStore()).isFalse();
+ }
+
+ @Test
+ public void sampleToLocalSpanStore_RunningSpan() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("Running span does not have the SampleToLocalSpanStore set.");
+ span.getSampleToLocalSpanStore();
+ }
+
+ @Test
+ public void getSpanKind() {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ Kind.SERVER,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ assertThat(span.getKind()).isEqualTo(Kind.SERVER);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/SpanBuilderImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/SpanBuilderImplTest.java
new file mode 100644
index 00000000..3267eac5
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/SpanBuilderImplTest.java
@@ -0,0 +1,404 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.internal.RandomHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.Span.Kind;
+import io.opencensus.trace.Span.Options;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.samplers.Samplers;
+import java.util.Collections;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link SpanBuilderImpl}. */
+@RunWith(JUnit4.class)
+public class SpanBuilderImplTest {
+ private static final String SPAN_NAME = "MySpanName";
+ private SpanBuilderImpl.Options spanBuilderOptions;
+ private final TraceParams alwaysSampleTraceParams =
+ TraceParams.DEFAULT.toBuilder().setSampler(Samplers.alwaysSample()).build();
+ private final TestClock testClock = TestClock.create();
+ private final RandomHandler randomHandler = new FakeRandomHandler();
+ @Mock private StartEndHandler startEndHandler;
+ @Mock private TraceConfig traceConfig;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ spanBuilderOptions =
+ new SpanBuilderImpl.Options(randomHandler, startEndHandler, testClock, traceConfig);
+ when(traceConfig.getActiveTraceParams()).thenReturn(alwaysSampleTraceParams);
+ }
+
+ @Test
+ public void startSpan_CreatesTheCorrectSpanImplInstance() {
+ assertThat(
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.alwaysSample())
+ .startSpan())
+ .isInstanceOf(RecordEventsSpanImpl.class);
+ assertThat(
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setRecordEvents(true)
+ .setSampler(Samplers.neverSample())
+ .startSpan())
+ .isInstanceOf(RecordEventsSpanImpl.class);
+ assertThat(
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .startSpan())
+ .isInstanceOf(NoRecordEventsSpanImpl.class);
+ }
+
+ @Test
+ public void setSpanKind_NotNull() {
+ RecordEventsSpanImpl span =
+ (RecordEventsSpanImpl)
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSpanKind(Kind.CLIENT)
+ .setRecordEvents(true)
+ .startSpan();
+ assertThat(span.getKind()).isEqualTo(Kind.CLIENT);
+ assertThat(span.toSpanData().getKind()).isEqualTo(Kind.CLIENT);
+ }
+
+ @Test
+ public void setSpanKind_DefaultNull() {
+ RecordEventsSpanImpl span =
+ (RecordEventsSpanImpl)
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setRecordEvents(true)
+ .startSpan();
+ assertThat(span.getKind()).isNull();
+ assertThat(span.toSpanData().getKind()).isNull();
+ }
+
+ @Test
+ public void startSpanNullParent() {
+ RecordEventsSpanImpl span =
+ (RecordEventsSpanImpl)
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setRecordEvents(true)
+ .startSpan();
+ assertThat(span.getContext().isValid()).isTrue();
+ assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+ assertThat(span.getContext().getTraceOptions().isSampled()).isTrue();
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getParentSpanId()).isNull();
+ assertThat(spanData.getHasRemoteParent()).isNull();
+ assertThat(spanData.getStartTimestamp()).isEqualTo(testClock.now());
+ assertThat(spanData.getName()).isEqualTo(SPAN_NAME);
+ }
+
+ @Test
+ public void startSpanNullParentWithRecordEvents() {
+ RecordEventsSpanImpl span =
+ (RecordEventsSpanImpl)
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .setRecordEvents(true)
+ .startSpan();
+ assertThat(span.getContext().isValid()).isTrue();
+ assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+ assertThat(span.getContext().getTraceOptions().isSampled()).isFalse();
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getParentSpanId()).isNull();
+ assertThat(spanData.getHasRemoteParent()).isNull();
+ }
+
+ @Test
+ public void startSpanNullParentNoRecordOptions() {
+ Span span =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ assertThat(span.getContext().isValid()).isTrue();
+ assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isFalse();
+ assertThat(span.getContext().getTraceOptions().isSampled()).isFalse();
+ }
+
+ @Test
+ public void startChildSpan() {
+ Span rootSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions).startSpan();
+ assertThat(rootSpan.getContext().isValid()).isTrue();
+ assertThat(rootSpan.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+ assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isTrue();
+ assertThat(((RecordEventsSpanImpl) rootSpan).toSpanData().getHasRemoteParent()).isNull();
+ Span childSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, rootSpan, spanBuilderOptions).startSpan();
+ assertThat(childSpan.getContext().isValid()).isTrue();
+ assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+ assertThat(((RecordEventsSpanImpl) childSpan).toSpanData().getParentSpanId())
+ .isEqualTo(rootSpan.getContext().getSpanId());
+ assertThat(((RecordEventsSpanImpl) childSpan).toSpanData().getHasRemoteParent()).isFalse();
+ assertThat(((RecordEventsSpanImpl) childSpan).getTimestampConverter())
+ .isEqualTo(((RecordEventsSpanImpl) rootSpan).getTimestampConverter());
+ }
+
+ @Test
+ public void startRemoteSpan_NullParent() {
+ RecordEventsSpanImpl span =
+ (RecordEventsSpanImpl)
+ SpanBuilderImpl.createWithRemoteParent(SPAN_NAME, null, spanBuilderOptions)
+ .setRecordEvents(true)
+ .startSpan();
+ assertThat(span.getContext().isValid()).isTrue();
+ assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+ assertThat(span.getContext().getTraceOptions().isSampled()).isTrue();
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getParentSpanId()).isNull();
+ assertThat(spanData.getHasRemoteParent()).isNull();
+ }
+
+ @Test
+ public void startRemoteSpanInvalidParent() {
+ RecordEventsSpanImpl span =
+ (RecordEventsSpanImpl)
+ SpanBuilderImpl.createWithRemoteParent(
+ SPAN_NAME, SpanContext.INVALID, spanBuilderOptions)
+ .startSpan();
+ assertThat(span.getContext().isValid()).isTrue();
+ assertThat(span.getOptions().contains(Options.RECORD_EVENTS)).isTrue();
+ assertThat(span.getContext().getTraceOptions().isSampled()).isTrue();
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getParentSpanId()).isNull();
+ assertThat(spanData.getHasRemoteParent()).isNull();
+ }
+
+ @Test
+ public void startRemoteSpan() {
+ SpanContext spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(randomHandler.current()),
+ SpanId.generateRandomId(randomHandler.current()),
+ TraceOptions.DEFAULT);
+ RecordEventsSpanImpl span =
+ (RecordEventsSpanImpl)
+ SpanBuilderImpl.createWithRemoteParent(SPAN_NAME, spanContext, spanBuilderOptions)
+ .setRecordEvents(true)
+ .startSpan();
+ assertThat(span.getContext().isValid()).isTrue();
+ assertThat(span.getContext().getTraceId()).isEqualTo(spanContext.getTraceId());
+ assertThat(span.getContext().getTraceOptions().isSampled()).isTrue();
+ SpanData spanData = span.toSpanData();
+ assertThat(spanData.getParentSpanId()).isEqualTo(spanContext.getSpanId());
+ assertThat(spanData.getHasRemoteParent()).isTrue();
+ }
+
+ @Test
+ public void startRootSpan_WithSpecifiedSampler() {
+ // Apply given sampler before default sampler for root spans.
+ Span rootSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ assertThat(rootSpan.getContext().isValid()).isTrue();
+ assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isFalse();
+ }
+
+ @Test
+ public void startRootSpan_WithoutSpecifiedSampler() {
+ // Apply default sampler (always true in the tests) for root spans.
+ Span rootSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions).startSpan();
+ assertThat(rootSpan.getContext().isValid()).isTrue();
+ assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isTrue();
+ }
+
+ @Test
+ public void startRemoteChildSpan_WithSpecifiedSampler() {
+ Span rootSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.alwaysSample())
+ .startSpan();
+ assertThat(rootSpan.getContext().isValid()).isTrue();
+ assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isTrue();
+ // Apply given sampler before default sampler for spans with remote parent.
+ Span childSpan =
+ SpanBuilderImpl.createWithRemoteParent(SPAN_NAME, rootSpan.getContext(), spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ assertThat(childSpan.getContext().isValid()).isTrue();
+ assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+ assertThat(childSpan.getContext().getTraceOptions().isSampled()).isFalse();
+ }
+
+ @Test
+ public void startRemoteChildSpan_WithoutSpecifiedSampler() {
+ Span rootSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ assertThat(rootSpan.getContext().isValid()).isTrue();
+ assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isFalse();
+ // Apply default sampler (always true in the tests) for spans with remote parent.
+ Span childSpan =
+ SpanBuilderImpl.createWithRemoteParent(SPAN_NAME, rootSpan.getContext(), spanBuilderOptions)
+ .startSpan();
+ assertThat(childSpan.getContext().isValid()).isTrue();
+ assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+ assertThat(childSpan.getContext().getTraceOptions().isSampled()).isTrue();
+ }
+
+ @Test
+ public void startChildSpan_WithSpecifiedSampler() {
+ Span rootSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.alwaysSample())
+ .startSpan();
+ assertThat(rootSpan.getContext().isValid()).isTrue();
+ assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isTrue();
+ // Apply the given sampler for child spans.
+ Span childSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, rootSpan, spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ assertThat(childSpan.getContext().isValid()).isTrue();
+ assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+ assertThat(childSpan.getContext().getTraceOptions().isSampled()).isFalse();
+ }
+
+ @Test
+ public void startChildSpan_WithoutSpecifiedSampler() {
+ Span rootSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ assertThat(rootSpan.getContext().isValid()).isTrue();
+ assertThat(rootSpan.getContext().getTraceOptions().isSampled()).isFalse();
+ // Don't apply the default sampler (always true) for child spans.
+ Span childSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, rootSpan, spanBuilderOptions).startSpan();
+ assertThat(childSpan.getContext().isValid()).isTrue();
+ assertThat(childSpan.getContext().getTraceId()).isEqualTo(rootSpan.getContext().getTraceId());
+ assertThat(childSpan.getContext().getTraceOptions().isSampled()).isFalse();
+ }
+
+ @Test
+ public void startChildSpan_SampledLinkedParent() {
+ Span rootSpanUnsampled =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.neverSample())
+ .startSpan();
+ assertThat(rootSpanUnsampled.getContext().getTraceOptions().isSampled()).isFalse();
+ Span rootSpanSampled =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, null, spanBuilderOptions)
+ .setSampler(Samplers.alwaysSample())
+ .startSpan();
+ assertThat(rootSpanSampled.getContext().getTraceOptions().isSampled()).isTrue();
+ // Sampled because the linked parent is sampled.
+ Span childSpan =
+ SpanBuilderImpl.createWithParent(SPAN_NAME, rootSpanUnsampled, spanBuilderOptions)
+ .setParentLinks(Collections.singletonList(rootSpanSampled))
+ .startSpan();
+ assertThat(childSpan.getContext().isValid()).isTrue();
+ assertThat(childSpan.getContext().getTraceId())
+ .isEqualTo(rootSpanUnsampled.getContext().getTraceId());
+ assertThat(childSpan.getContext().getTraceOptions().isSampled()).isTrue();
+ }
+
+ @Test
+ public void startRemoteChildSpan_WithProbabilitySamplerDefaultSampler() {
+ when(traceConfig.getActiveTraceParams()).thenReturn(TraceParams.DEFAULT);
+ // This traceId will not be sampled by the ProbabilitySampler because the first 8 bytes as long
+ // is not less than probability * Long.MAX_VALUE;
+ TraceId traceId =
+ TraceId.fromBytes(
+ new byte[] {
+ (byte) 0x8F,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ (byte) 0xFF,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ });
+
+ // If parent is sampled then the remote child must be sampled.
+ Span childSpan =
+ SpanBuilderImpl.createWithRemoteParent(
+ SPAN_NAME,
+ SpanContext.create(
+ traceId,
+ SpanId.generateRandomId(randomHandler.current()),
+ TraceOptions.builder().setIsSampled(true).build()),
+ spanBuilderOptions)
+ .startSpan();
+ assertThat(childSpan.getContext().isValid()).isTrue();
+ assertThat(childSpan.getContext().getTraceId()).isEqualTo(traceId);
+ assertThat(childSpan.getContext().getTraceOptions().isSampled()).isTrue();
+ childSpan.end();
+
+ assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+
+ // If parent is not sampled then the remote child must be not sampled.
+ childSpan =
+ SpanBuilderImpl.createWithRemoteParent(
+ SPAN_NAME,
+ SpanContext.create(
+ traceId,
+ SpanId.generateRandomId(randomHandler.current()),
+ TraceOptions.DEFAULT),
+ spanBuilderOptions)
+ .startSpan();
+ assertThat(childSpan.getContext().isValid()).isTrue();
+ assertThat(childSpan.getContext().getTraceId()).isEqualTo(traceId);
+ assertThat(childSpan.getContext().getTraceOptions().isSampled()).isFalse();
+ childSpan.end();
+ }
+
+ private static final class FakeRandomHandler extends RandomHandler {
+ private final Random random;
+
+ FakeRandomHandler() {
+ this.random = new Random(1234);
+ }
+
+ @Override
+ public Random current() {
+ return random;
+ }
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/TraceComponentImplBaseTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/TraceComponentImplBaseTest.java
new file mode 100644
index 00000000..9f468442
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/TraceComponentImplBaseTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.export.ExportComponentImpl;
+import io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler;
+import io.opencensus.implcore.trace.propagation.PropagationComponentImpl;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceComponentImplBase}. */
+@RunWith(JUnit4.class)
+public class TraceComponentImplBaseTest {
+ private final TraceComponentImplBase traceComponentImplBase =
+ new TraceComponentImplBase(
+ MillisClock.getInstance(), new SecureRandomHandler(), new SimpleEventQueue());
+
+ @Test
+ public void implementationOfTracer() {
+ assertThat(traceComponentImplBase.getTracer()).isInstanceOf(TracerImpl.class);
+ }
+
+ @Test
+ public void implementationOfBinaryPropagationHandler() {
+ assertThat(traceComponentImplBase.getPropagationComponent())
+ .isInstanceOf(PropagationComponentImpl.class);
+ }
+
+ @Test
+ public void implementationOfClock() {
+ assertThat(traceComponentImplBase.getClock()).isInstanceOf(MillisClock.class);
+ }
+
+ @Test
+ public void implementationOfTraceExporter() {
+ assertThat(traceComponentImplBase.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/TracerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/TracerImplTest.java
new file mode 100644
index 00000000..d10be6a2
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/TracerImplTest.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.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.BlankSpan;
+import io.opencensus.trace.SpanBuilder;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.config.TraceConfig;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link TracerImpl}. */
+@RunWith(JUnit4.class)
+public class TracerImplTest {
+ private static final String SPAN_NAME = "MySpanName";
+ @Mock private StartEndHandler startEndHandler;
+ @Mock private TraceConfig traceConfig;
+ private TracerImpl tracer;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ tracer =
+ new TracerImpl(new SecureRandomHandler(), startEndHandler, TestClock.create(), traceConfig);
+ }
+
+ @Test
+ public void createSpanBuilder() {
+ SpanBuilder spanBuilder = tracer.spanBuilderWithExplicitParent(SPAN_NAME, BlankSpan.INSTANCE);
+ assertThat(spanBuilder).isInstanceOf(SpanBuilderImpl.class);
+ }
+
+ @Test
+ public void createSpanBuilderWithRemoteParet() {
+ SpanBuilder spanBuilder = tracer.spanBuilderWithRemoteParent(SPAN_NAME, SpanContext.INVALID);
+ assertThat(spanBuilder).isInstanceOf(SpanBuilderImpl.class);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/config/TraceConfigImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/config/TraceConfigImplTest.java
new file mode 100644
index 00000000..ecaeda6d
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/config/TraceConfigImplTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.trace.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.samplers.Samplers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceConfigImpl}. */
+@RunWith(JUnit4.class)
+public class TraceConfigImplTest {
+ private final TraceConfigImpl traceConfig = new TraceConfigImpl();
+
+ @Test
+ public void defaultActiveTraceParams() {
+ assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+ }
+
+ @Test
+ public void updateTraceParams() {
+ TraceParams traceParams =
+ TraceParams.DEFAULT
+ .toBuilder()
+ .setSampler(Samplers.alwaysSample())
+ .setMaxNumberOfAttributes(8)
+ .setMaxNumberOfAnnotations(9)
+ .setMaxNumberOfNetworkEvents(10)
+ .setMaxNumberOfLinks(11)
+ .build();
+ traceConfig.updateActiveTraceParams(traceParams);
+ assertThat(traceConfig.getActiveTraceParams()).isEqualTo(traceParams);
+ traceConfig.updateActiveTraceParams(TraceParams.DEFAULT);
+ assertThat(traceConfig.getActiveTraceParams()).isEqualTo(TraceParams.DEFAULT);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/ExportComponentImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/ExportComponentImplTest.java
new file mode 100644
index 00000000..4b8993ff
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/ExportComponentImplTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.trace.export.ExportComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link ExportComponentImpl}. */
+@RunWith(JUnit4.class)
+public class ExportComponentImplTest {
+ private final ExportComponent exportComponentWithInProcess =
+ ExportComponentImpl.createWithInProcessStores(new SimpleEventQueue());
+ private final ExportComponent exportComponentWithoutInProcess =
+ ExportComponentImpl.createWithoutInProcessStores(new SimpleEventQueue());
+
+ @Test
+ public void implementationOfSpanExporter() {
+ assertThat(exportComponentWithInProcess.getSpanExporter()).isInstanceOf(SpanExporterImpl.class);
+ }
+
+ @Test
+ public void implementationOfActiveSpans() {
+ assertThat(exportComponentWithInProcess.getRunningSpanStore())
+ .isInstanceOf(InProcessRunningSpanStoreImpl.class);
+ assertThat(exportComponentWithoutInProcess.getRunningSpanStore())
+ .isInstanceOf(RunningSpanStoreImpl.getNoopRunningSpanStoreImpl().getClass());
+ }
+
+ @Test
+ public void implementationOfSampledSpanStore() {
+ assertThat(exportComponentWithInProcess.getSampledSpanStore())
+ .isInstanceOf(InProcessSampledSpanStoreImpl.class);
+ assertThat(exportComponentWithoutInProcess.getSampledSpanStore())
+ .isInstanceOf(SampledSpanStoreImpl.getNoopSampledSpanStoreImpl().getClass());
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImplTest.java
new file mode 100644
index 00000000..68ce1c18
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessRunningSpanStoreImplTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.StartEndHandlerImpl;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.RunningSpanStore.Filter;
+import java.util.Random;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InProcessRunningSpanStoreImpl}. */
+@RunWith(JUnit4.class)
+public class InProcessRunningSpanStoreImplTest {
+
+ private static final String SPAN_NAME_1 = "MySpanName/1";
+ private static final String SPAN_NAME_2 = "MySpanName/2";
+ private final Random random = new Random(1234);
+ private final SpanExporterImpl sampledSpansServiceExporter =
+ SpanExporterImpl.create(4, Duration.create(1, 0));
+ private final InProcessRunningSpanStoreImpl activeSpansExporter =
+ new InProcessRunningSpanStoreImpl();
+ private final StartEndHandler startEndHandler =
+ new StartEndHandlerImpl(
+ sampledSpansServiceExporter, activeSpansExporter, null, new SimpleEventQueue());
+
+ private RecordEventsSpanImpl createSpan(String spanName) {
+ final SpanContext spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ TraceOptions.DEFAULT);
+ return RecordEventsSpanImpl.startSpan(
+ spanContext,
+ spanName,
+ null,
+ SpanId.generateRandomId(random),
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ null,
+ MillisClock.getInstance());
+ }
+
+ @Test
+ public void getSummary_SpansWithDifferentNames() {
+ final RecordEventsSpanImpl span1 = createSpan(SPAN_NAME_1);
+ final RecordEventsSpanImpl span2 = createSpan(SPAN_NAME_2);
+ assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(2);
+ assertThat(
+ activeSpansExporter
+ .getSummary()
+ .getPerSpanNameSummary()
+ .get(SPAN_NAME_1)
+ .getNumRunningSpans())
+ .isEqualTo(1);
+ assertThat(
+ activeSpansExporter
+ .getSummary()
+ .getPerSpanNameSummary()
+ .get(SPAN_NAME_2)
+ .getNumRunningSpans())
+ .isEqualTo(1);
+ span1.end();
+ assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(1);
+ assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().get(SPAN_NAME_1)).isNull();
+ assertThat(
+ activeSpansExporter
+ .getSummary()
+ .getPerSpanNameSummary()
+ .get(SPAN_NAME_2)
+ .getNumRunningSpans())
+ .isEqualTo(1);
+ span2.end();
+ assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(0);
+ }
+
+ @Test
+ public void getSummary_SpansWithSameName() {
+ final RecordEventsSpanImpl span1 = createSpan(SPAN_NAME_1);
+ final RecordEventsSpanImpl span2 = createSpan(SPAN_NAME_1);
+ final RecordEventsSpanImpl span3 = createSpan(SPAN_NAME_1);
+ assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(1);
+ assertThat(
+ activeSpansExporter
+ .getSummary()
+ .getPerSpanNameSummary()
+ .get(SPAN_NAME_1)
+ .getNumRunningSpans())
+ .isEqualTo(3);
+ span1.end();
+ assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(1);
+ assertThat(
+ activeSpansExporter
+ .getSummary()
+ .getPerSpanNameSummary()
+ .get(SPAN_NAME_1)
+ .getNumRunningSpans())
+ .isEqualTo(2);
+ span2.end();
+ assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(1);
+ assertThat(
+ activeSpansExporter
+ .getSummary()
+ .getPerSpanNameSummary()
+ .get(SPAN_NAME_1)
+ .getNumRunningSpans())
+ .isEqualTo(1);
+ span3.end();
+ assertThat(activeSpansExporter.getSummary().getPerSpanNameSummary().size()).isEqualTo(0);
+ }
+
+ @Test
+ public void getActiveSpans_SpansWithDifferentNames() {
+ RecordEventsSpanImpl span1 = createSpan(SPAN_NAME_1);
+ RecordEventsSpanImpl span2 = createSpan(SPAN_NAME_2);
+ assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 0)))
+ .containsExactly(span1.toSpanData());
+ assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 2)))
+ .containsExactly(span1.toSpanData());
+ assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_2, 0)))
+ .containsExactly(span2.toSpanData());
+ span1.end();
+ span2.end();
+ }
+
+ @Test
+ public void getActiveSpans_SpansWithSameName() {
+ RecordEventsSpanImpl span1 = createSpan(SPAN_NAME_1);
+ RecordEventsSpanImpl span2 = createSpan(SPAN_NAME_1);
+ RecordEventsSpanImpl span3 = createSpan(SPAN_NAME_1);
+ assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 0)))
+ .containsExactly(span1.toSpanData(), span2.toSpanData(), span3.toSpanData());
+ assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 2)).size())
+ .isEqualTo(2);
+ assertThat(activeSpansExporter.getRunningSpans(Filter.create(SPAN_NAME_1, 2)))
+ .containsAnyOf(span1.toSpanData(), span2.toSpanData(), span3.toSpanData());
+ span1.end();
+ span2.end();
+ span3.end();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImplTest.java
new file mode 100644
index 00000000..7d8b434e
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/InProcessSampledSpanStoreImplTest.java
@@ -0,0 +1,368 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.EndSpanOptions;
+import io.opencensus.trace.Span;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.Status;
+import io.opencensus.trace.Status.CanonicalCode;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SampledSpanStore.ErrorFilter;
+import io.opencensus.trace.export.SampledSpanStore.LatencyBucketBoundaries;
+import io.opencensus.trace.export.SampledSpanStore.LatencyFilter;
+import io.opencensus.trace.export.SampledSpanStore.PerSpanNameSummary;
+import io.opencensus.trace.export.SpanData;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link InProcessSampledSpanStoreImpl}. */
+@RunWith(JUnit4.class)
+public class InProcessSampledSpanStoreImplTest {
+ private static final String REGISTERED_SPAN_NAME = "MySpanName/1";
+ private static final String NOT_REGISTERED_SPAN_NAME = "MySpanName/2";
+ private static final long NUM_NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
+ private final Random random = new Random(1234);
+ private final SpanContext sampledSpanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ TraceOptions.builder().setIsSampled(true).build());
+ private final SpanContext notSampledSpanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+ private final SpanId parentSpanId = SpanId.generateRandomId(random);
+ private final TestClock testClock = TestClock.create(Timestamp.create(12345, 54321));
+ private final InProcessSampledSpanStoreImpl sampleStore =
+ new InProcessSampledSpanStoreImpl(new SimpleEventQueue());
+ private final StartEndHandler startEndHandler =
+ new StartEndHandler() {
+ @Override
+ public void onStart(RecordEventsSpanImpl span) {
+ // Do nothing.
+ }
+
+ @Override
+ public void onEnd(RecordEventsSpanImpl span) {
+ sampleStore.considerForSampling(span);
+ }
+ };
+
+ @Before
+ public void setUp() {
+ sampleStore.registerSpanNamesForCollection(Collections.singletonList(REGISTERED_SPAN_NAME));
+ }
+
+ private RecordEventsSpanImpl createSampledSpan(String spanName) {
+ return RecordEventsSpanImpl.startSpan(
+ sampledSpanContext,
+ spanName,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ null,
+ testClock);
+ }
+
+ private RecordEventsSpanImpl createNotSampledSpan(String spanName) {
+ return RecordEventsSpanImpl.startSpan(
+ notSampledSpanContext,
+ spanName,
+ null,
+ parentSpanId,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ null,
+ testClock);
+ }
+
+ private void addSpanNameToAllLatencyBuckets(String spanName) {
+ for (LatencyBucketBoundaries boundaries : LatencyBucketBoundaries.values()) {
+ Span sampledSpan = createSampledSpan(spanName);
+ Span notSampledSpan = createNotSampledSpan(spanName);
+ if (boundaries.getLatencyLowerNs() < NUM_NANOS_PER_SECOND) {
+ testClock.advanceTime(Duration.create(0, (int) boundaries.getLatencyLowerNs()));
+ } else {
+ testClock.advanceTime(
+ Duration.create(
+ boundaries.getLatencyLowerNs() / NUM_NANOS_PER_SECOND,
+ (int) (boundaries.getLatencyLowerNs() % NUM_NANOS_PER_SECOND)));
+ }
+ sampledSpan.end();
+ notSampledSpan.end();
+ }
+ }
+
+ private void addSpanNameToAllErrorBuckets(String spanName) {
+ for (CanonicalCode code : CanonicalCode.values()) {
+ if (code != CanonicalCode.OK) {
+ Span sampledSpan = createSampledSpan(spanName);
+ Span notSampledSpan = createNotSampledSpan(spanName);
+ testClock.advanceTime(Duration.create(0, 1000));
+ sampledSpan.end(EndSpanOptions.builder().setStatus(code.toStatus()).build());
+ notSampledSpan.end(EndSpanOptions.builder().setStatus(code.toStatus()).build());
+ }
+ }
+ }
+
+ @Test
+ public void addSpansWithRegisteredNamesInAllLatencyBuckets() {
+ addSpanNameToAllLatencyBuckets(REGISTERED_SPAN_NAME);
+ Map<String, PerSpanNameSummary> perSpanNameSummary =
+ sampleStore.getSummary().getPerSpanNameSummary();
+ assertThat(perSpanNameSummary.size()).isEqualTo(1);
+ Map<LatencyBucketBoundaries, Integer> latencyBucketsSummaries =
+ perSpanNameSummary.get(REGISTERED_SPAN_NAME).getNumbersOfLatencySampledSpans();
+ assertThat(latencyBucketsSummaries.size()).isEqualTo(LatencyBucketBoundaries.values().length);
+ for (Map.Entry<LatencyBucketBoundaries, Integer> it : latencyBucketsSummaries.entrySet()) {
+ assertThat(it.getValue()).isEqualTo(2);
+ }
+ }
+
+ @Test
+ public void addSpansWithoutRegisteredNamesInAllLatencyBuckets() {
+ addSpanNameToAllLatencyBuckets(NOT_REGISTERED_SPAN_NAME);
+ Map<String, PerSpanNameSummary> perSpanNameSummary =
+ sampleStore.getSummary().getPerSpanNameSummary();
+ assertThat(perSpanNameSummary.size()).isEqualTo(1);
+ assertThat(perSpanNameSummary.containsKey(NOT_REGISTERED_SPAN_NAME)).isFalse();
+ }
+
+ @Test
+ public void registerUnregisterAndListSpanNames() {
+ assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+ .containsExactly(REGISTERED_SPAN_NAME);
+ sampleStore.registerSpanNamesForCollection(Collections.singletonList(NOT_REGISTERED_SPAN_NAME));
+ assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+ .containsExactly(REGISTERED_SPAN_NAME, NOT_REGISTERED_SPAN_NAME);
+ sampleStore.unregisterSpanNamesForCollection(
+ Collections.singletonList(NOT_REGISTERED_SPAN_NAME));
+ assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+ .containsExactly(REGISTERED_SPAN_NAME);
+ }
+
+ @Test
+ public void registerSpanNamesViaSpanBuilderOption() {
+ assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+ .containsExactly(REGISTERED_SPAN_NAME);
+ createSampledSpan(NOT_REGISTERED_SPAN_NAME)
+ .end(EndSpanOptions.builder().setSampleToLocalSpanStore(true).build());
+ assertThat(sampleStore.getRegisteredSpanNamesForCollection())
+ .containsExactly(REGISTERED_SPAN_NAME, NOT_REGISTERED_SPAN_NAME);
+ }
+
+ @Test
+ public void addSpansWithRegisteredNamesInAllErrorBuckets() {
+ addSpanNameToAllErrorBuckets(REGISTERED_SPAN_NAME);
+ Map<String, PerSpanNameSummary> perSpanNameSummary =
+ sampleStore.getSummary().getPerSpanNameSummary();
+ assertThat(perSpanNameSummary.size()).isEqualTo(1);
+ Map<CanonicalCode, Integer> errorBucketsSummaries =
+ perSpanNameSummary.get(REGISTERED_SPAN_NAME).getNumbersOfErrorSampledSpans();
+ assertThat(errorBucketsSummaries.size()).isEqualTo(CanonicalCode.values().length - 1);
+ for (Map.Entry<CanonicalCode, Integer> it : errorBucketsSummaries.entrySet()) {
+ assertThat(it.getValue()).isEqualTo(2);
+ }
+ }
+
+ @Test
+ public void addSpansWithoutRegisteredNamesInAllErrorBuckets() {
+ addSpanNameToAllErrorBuckets(NOT_REGISTERED_SPAN_NAME);
+ Map<String, PerSpanNameSummary> perSpanNameSummary =
+ sampleStore.getSummary().getPerSpanNameSummary();
+ assertThat(perSpanNameSummary.size()).isEqualTo(1);
+ assertThat(perSpanNameSummary.containsKey(NOT_REGISTERED_SPAN_NAME)).isFalse();
+ }
+
+ @Test
+ public void getErrorSampledSpans() {
+ RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, 1000));
+ span.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+ Collection<SpanData> samples =
+ sampleStore.getErrorSampledSpans(
+ ErrorFilter.create(REGISTERED_SPAN_NAME, CanonicalCode.CANCELLED, 0));
+ assertThat(samples.size()).isEqualTo(1);
+ assertThat(samples.contains(span.toSpanData())).isTrue();
+ }
+
+ @Test
+ public void getErrorSampledSpans_MaxSpansToReturn() {
+ RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, 1000));
+ span1.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+ // Advance time to allow other spans to be sampled.
+ testClock.advanceTime(Duration.create(5, 0));
+ RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, 1000));
+ span2.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+ Collection<SpanData> samples =
+ sampleStore.getErrorSampledSpans(
+ ErrorFilter.create(REGISTERED_SPAN_NAME, CanonicalCode.CANCELLED, 1));
+ assertThat(samples.size()).isEqualTo(1);
+ // No order guaranteed so one of the spans should be in the list.
+ assertThat(samples).containsAnyOf(span1.toSpanData(), span2.toSpanData());
+ }
+
+ @Test
+ public void getErrorSampledSpans_NullCode() {
+ RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, 1000));
+ span1.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+ RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, 1000));
+ span2.end(EndSpanOptions.builder().setStatus(Status.UNKNOWN).build());
+ Collection<SpanData> samples =
+ sampleStore.getErrorSampledSpans(ErrorFilter.create(REGISTERED_SPAN_NAME, null, 0));
+ assertThat(samples.size()).isEqualTo(2);
+ assertThat(samples).containsExactly(span1.toSpanData(), span2.toSpanData());
+ }
+
+ @Test
+ public void getErrorSampledSpans_NullCode_MaxSpansToReturn() {
+ RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, 1000));
+ span1.end(EndSpanOptions.builder().setStatus(Status.CANCELLED).build());
+ RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, 1000));
+ span2.end(EndSpanOptions.builder().setStatus(Status.UNKNOWN).build());
+ Collection<SpanData> samples =
+ sampleStore.getErrorSampledSpans(ErrorFilter.create(REGISTERED_SPAN_NAME, null, 1));
+ assertThat(samples.size()).isEqualTo(1);
+ assertThat(samples).containsAnyOf(span1.toSpanData(), span2.toSpanData());
+ }
+
+ @Test
+ public void getLatencySampledSpans() {
+ RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+ span.end();
+ Collection<SpanData> samples =
+ sampleStore.getLatencySampledSpans(
+ LatencyFilter.create(
+ REGISTERED_SPAN_NAME,
+ TimeUnit.MICROSECONDS.toNanos(15),
+ TimeUnit.MICROSECONDS.toNanos(25),
+ 0));
+ assertThat(samples.size()).isEqualTo(1);
+ assertThat(samples.contains(span.toSpanData())).isTrue();
+ }
+
+ @Test
+ public void getLatencySampledSpans_ExclusiveUpperBound() {
+ RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+ span.end();
+ Collection<SpanData> samples =
+ sampleStore.getLatencySampledSpans(
+ LatencyFilter.create(
+ REGISTERED_SPAN_NAME,
+ TimeUnit.MICROSECONDS.toNanos(15),
+ TimeUnit.MICROSECONDS.toNanos(20),
+ 0));
+ assertThat(samples.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void getLatencySampledSpans_InclusiveLowerBound() {
+ RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+ span.end();
+ Collection<SpanData> samples =
+ sampleStore.getLatencySampledSpans(
+ LatencyFilter.create(
+ REGISTERED_SPAN_NAME,
+ TimeUnit.MICROSECONDS.toNanos(20),
+ TimeUnit.MICROSECONDS.toNanos(25),
+ 0));
+ assertThat(samples.size()).isEqualTo(1);
+ assertThat(samples.contains(span.toSpanData())).isTrue();
+ }
+
+ @Test
+ public void getLatencySampledSpans_QueryBetweenMultipleBuckets() {
+ RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+ span1.end();
+ // Advance time to allow other spans to be sampled.
+ testClock.advanceTime(Duration.create(5, 0));
+ RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(200)));
+ span2.end();
+ Collection<SpanData> samples =
+ sampleStore.getLatencySampledSpans(
+ LatencyFilter.create(
+ REGISTERED_SPAN_NAME,
+ TimeUnit.MICROSECONDS.toNanos(15),
+ TimeUnit.MICROSECONDS.toNanos(250),
+ 0));
+ assertThat(samples).containsExactly(span1.toSpanData(), span2.toSpanData());
+ }
+
+ @Test
+ public void getLatencySampledSpans_MaxSpansToReturn() {
+ RecordEventsSpanImpl span1 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(20)));
+ span1.end();
+ // Advance time to allow other spans to be sampled.
+ testClock.advanceTime(Duration.create(5, 0));
+ RecordEventsSpanImpl span2 = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(200)));
+ span2.end();
+ Collection<SpanData> samples =
+ sampleStore.getLatencySampledSpans(
+ LatencyFilter.create(
+ REGISTERED_SPAN_NAME,
+ TimeUnit.MICROSECONDS.toNanos(15),
+ TimeUnit.MICROSECONDS.toNanos(250),
+ 1));
+ assertThat(samples.size()).isEqualTo(1);
+ assertThat(samples.contains(span1.toSpanData())).isTrue();
+ }
+
+ @Test
+ public void ignoreNegativeSpanLatency() {
+ RecordEventsSpanImpl span = createSampledSpan(REGISTERED_SPAN_NAME);
+ testClock.advanceTime(Duration.create(0, (int) TimeUnit.MICROSECONDS.toNanos(-20)));
+ span.end();
+ Collection<SpanData> samples =
+ sampleStore.getLatencySampledSpans(
+ LatencyFilter.create(REGISTERED_SPAN_NAME, 0, Long.MAX_VALUE, 0));
+ assertThat(samples.size()).isEqualTo(0);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopRunningSpanStoreImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopRunningSpanStoreImplTest.java
new file mode 100644
index 00000000..96669df7
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopRunningSpanStoreImplTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2018, 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.RunningSpanStore.Filter;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link RunningSpanStoreImpl.NoopRunningSpanStoreImpl}. */
+@RunWith(JUnit4.class)
+public class NoopRunningSpanStoreImplTest {
+
+ private static final String SPAN_NAME = "MySpanName";
+
+ private final Timestamp timestamp = Timestamp.create(1234, 5678);
+ private final Random random = new Random(1234);
+ private final SpanContext spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+ private final TestClock testClock = TestClock.create(timestamp);
+ private final TimestampConverter timestampConverter = TimestampConverter.now(testClock);
+ @Mock private StartEndHandler startEndHandler;
+ private RecordEventsSpanImpl recordEventsSpanImpl;
+ // maxSpansToReturn=0 means all
+ private final Filter filter = Filter.create(SPAN_NAME, 0 /* maxSpansToReturn */);
+ private final EventQueue eventQueue = new SimpleEventQueue();
+ private final RunningSpanStoreImpl runningSpanStoreImpl =
+ ExportComponentImpl.createWithoutInProcessStores(eventQueue).getRunningSpanStore();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ recordEventsSpanImpl =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ null,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ }
+
+ private void getMethodsShouldReturnEmpty() {
+ // get methods should always return empty collections.
+ assertThat(runningSpanStoreImpl.getSummary().getPerSpanNameSummary()).isEmpty();
+ assertThat(runningSpanStoreImpl.getRunningSpans(filter)).isEmpty();
+ }
+
+ @Test
+ public void noopImplementation() {
+ getMethodsShouldReturnEmpty();
+ // onStart() does not affect the result.
+ runningSpanStoreImpl.onStart(recordEventsSpanImpl);
+ getMethodsShouldReturnEmpty();
+ // onEnd() does not affect the result.
+ runningSpanStoreImpl.onEnd(recordEventsSpanImpl);
+ getMethodsShouldReturnEmpty();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopSampledSpanStoreImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopSampledSpanStoreImplTest.java
new file mode 100644
index 00000000..b9fbd432
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/NoopSampledSpanStoreImplTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2018, 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.internal.TimestampConverter;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.testing.common.TestClock;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SampledSpanStore.ErrorFilter;
+import io.opencensus.trace.export.SampledSpanStore.LatencyFilter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link SampledSpanStoreImpl.NoopSampledSpanStoreImpl}. */
+@RunWith(JUnit4.class)
+public final class NoopSampledSpanStoreImplTest {
+
+ private static final String SPAN_NAME = "MySpanName";
+ private static final Collection<String> NAMES_FOR_COLLECTION =
+ Collections.<String>singletonList(SPAN_NAME);
+
+ private final Timestamp timestamp = Timestamp.create(1234, 5678);
+ private final Random random = new Random(1234);
+ private final SpanContext spanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+ private final TestClock testClock = TestClock.create(timestamp);
+ private final TimestampConverter timestampConverter = TimestampConverter.now(testClock);
+ @Mock private StartEndHandler startEndHandler;
+ private RecordEventsSpanImpl recordEventsSpanImpl;
+ // maxSpansToReturn=0 means all
+ private final ErrorFilter errorFilter =
+ ErrorFilter.create(SPAN_NAME, null /* canonicalCode */, 0 /* maxSpansToReturn */);
+ private final LatencyFilter latencyFilter =
+ LatencyFilter.create(
+ SPAN_NAME,
+ 0 /* latencyLowerNs */,
+ Long.MAX_VALUE /* latencyUpperNs */,
+ 0 /* maxSpansToReturn */);
+ private final EventQueue eventQueue = new SimpleEventQueue();
+ private final SampledSpanStoreImpl sampledSpanStoreImpl =
+ ExportComponentImpl.createWithoutInProcessStores(eventQueue).getSampledSpanStore();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private void getMethodsShouldReturnEmpty() {
+ // get methods always return empty collections.
+ assertThat(sampledSpanStoreImpl.getSummary().getPerSpanNameSummary()).isEmpty();
+ assertThat(sampledSpanStoreImpl.getRegisteredSpanNamesForCollection()).isEmpty();
+ assertThat(sampledSpanStoreImpl.getErrorSampledSpans(errorFilter)).isEmpty();
+ assertThat(sampledSpanStoreImpl.getLatencySampledSpans(latencyFilter)).isEmpty();
+ }
+
+ @Test
+ public void noopImplementation() {
+ // None of the get methods should yield non-empty result.
+ getMethodsShouldReturnEmpty();
+
+ // registerSpanNamesForCollection() should do nothing and do not affect the result.
+ sampledSpanStoreImpl.registerSpanNamesForCollection(NAMES_FOR_COLLECTION);
+ getMethodsShouldReturnEmpty();
+
+ // considerForSampling() should do nothing and do not affect the result.
+ // It should be called after registerSpanNamesForCollection.
+ recordEventsSpanImpl =
+ RecordEventsSpanImpl.startSpan(
+ spanContext,
+ SPAN_NAME,
+ null,
+ null,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ timestampConverter,
+ testClock);
+ recordEventsSpanImpl.end();
+ sampledSpanStoreImpl.considerForSampling(recordEventsSpanImpl);
+ getMethodsShouldReturnEmpty();
+
+ // unregisterSpanNamesForCollection() should do nothing and do not affect the result.
+ sampledSpanStoreImpl.unregisterSpanNamesForCollection(NAMES_FOR_COLLECTION);
+ getMethodsShouldReturnEmpty();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/export/SpanExporterImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/export/SpanExporterImplTest.java
new file mode 100644
index 00000000..f8f1d917
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/export/SpanExporterImplTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.trace.export;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.anyListOf;
+import static org.mockito.Mockito.doThrow;
+
+import io.opencensus.common.Duration;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl;
+import io.opencensus.implcore.trace.RecordEventsSpanImpl.StartEndHandler;
+import io.opencensus.implcore.trace.StartEndHandlerImpl;
+import io.opencensus.testing.export.TestHandler;
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.config.TraceParams;
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter.Handler;
+import java.util.List;
+import java.util.Random;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link SpanExporterImpl}. */
+@RunWith(JUnit4.class)
+public class SpanExporterImplTest {
+ private static final String SPAN_NAME_1 = "MySpanName/1";
+ private static final String SPAN_NAME_2 = "MySpanName/2";
+ private final Random random = new Random(1234);
+ private final SpanContext sampledSpanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random),
+ SpanId.generateRandomId(random),
+ TraceOptions.builder().setIsSampled(true).build());
+ private final SpanContext notSampledSpanContext =
+ SpanContext.create(
+ TraceId.generateRandomId(random), SpanId.generateRandomId(random), TraceOptions.DEFAULT);
+ private final RunningSpanStoreImpl runningSpanStore = new InProcessRunningSpanStoreImpl();
+ private final TestHandler serviceHandler = new TestHandler();
+ @Mock private Handler mockServiceHandler;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ private RecordEventsSpanImpl createSampledEndedSpan(
+ StartEndHandler startEndHandler, String spanName) {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ sampledSpanContext,
+ spanName,
+ null,
+ null,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ null,
+ MillisClock.getInstance());
+ span.end();
+ return span;
+ }
+
+ private RecordEventsSpanImpl createNotSampledEndedSpan(
+ StartEndHandler startEndHandler, String spanName) {
+ RecordEventsSpanImpl span =
+ RecordEventsSpanImpl.startSpan(
+ notSampledSpanContext,
+ spanName,
+ null,
+ null,
+ false,
+ TraceParams.DEFAULT,
+ startEndHandler,
+ null,
+ MillisClock.getInstance());
+ span.end();
+ return span;
+ }
+
+ @Test
+ public void exportDifferentSampledSpans() {
+ SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+ StartEndHandler startEndHandler =
+ new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+ spanExporter.registerHandler("test.service", serviceHandler);
+
+ RecordEventsSpanImpl span1 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_2);
+ List<SpanData> exported = serviceHandler.waitForExport(2);
+ assertThat(exported).containsExactly(span1.toSpanData(), span2.toSpanData());
+ }
+
+ @Test
+ public void exportMoreSpansThanTheBufferSize() {
+ SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+ StartEndHandler startEndHandler =
+ new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+ spanExporter.registerHandler("test.service", serviceHandler);
+
+ RecordEventsSpanImpl span1 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ RecordEventsSpanImpl span3 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ RecordEventsSpanImpl span4 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ RecordEventsSpanImpl span5 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ RecordEventsSpanImpl span6 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ List<SpanData> exported = serviceHandler.waitForExport(6);
+ assertThat(exported)
+ .containsExactly(
+ span1.toSpanData(),
+ span2.toSpanData(),
+ span3.toSpanData(),
+ span4.toSpanData(),
+ span5.toSpanData(),
+ span6.toSpanData());
+ }
+
+ @Test
+ public void interruptWorkerThreadStops() throws InterruptedException {
+ SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+
+ spanExporter.registerHandler("test.service", serviceHandler);
+
+ Thread serviceExporterThread = spanExporter.getServiceExporterThread();
+ serviceExporterThread.interrupt();
+ // Test that the worker thread will stop.
+ serviceExporterThread.join();
+ }
+
+ @Test
+ public void serviceHandlerThrowsException() {
+ doThrow(new IllegalArgumentException("No export for you."))
+ .when(mockServiceHandler)
+ .export(anyListOf(SpanData.class));
+
+ SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+ StartEndHandler startEndHandler =
+ new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+ spanExporter.registerHandler("test.service", serviceHandler);
+
+ spanExporter.registerHandler("mock.service", mockServiceHandler);
+ RecordEventsSpanImpl span1 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ List<SpanData> exported = serviceHandler.waitForExport(1);
+ assertThat(exported).containsExactly(span1.toSpanData());
+ // Continue to export after the exception was received.
+ RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ exported = serviceHandler.waitForExport(1);
+ assertThat(exported).containsExactly(span2.toSpanData());
+ }
+
+ @Test
+ public void exportSpansToMultipleServices() {
+ SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+ StartEndHandler startEndHandler =
+ new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+ spanExporter.registerHandler("test.service", serviceHandler);
+
+ TestHandler serviceHandler2 = new TestHandler();
+ spanExporter.registerHandler("test.service2", serviceHandler2);
+ RecordEventsSpanImpl span1 = createSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_2);
+ List<SpanData> exported1 = serviceHandler.waitForExport(2);
+ List<SpanData> exported2 = serviceHandler2.waitForExport(2);
+ assertThat(exported1).containsExactly(span1.toSpanData(), span2.toSpanData());
+ assertThat(exported2).containsExactly(span1.toSpanData(), span2.toSpanData());
+ }
+
+ @Test
+ public void exportNotSampledSpans() {
+ SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(1, 0));
+ StartEndHandler startEndHandler =
+ new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+ spanExporter.registerHandler("test.service", serviceHandler);
+
+ RecordEventsSpanImpl span1 = createNotSampledEndedSpan(startEndHandler, SPAN_NAME_1);
+ RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_2);
+ // Spans are recorded and exported in the same order as they are ended, we test that a non
+ // sampled span is not exported by creating and ending a sampled span after a non sampled span
+ // and checking that the first exported span is the sampled span (the non sampled did not get
+ // exported).
+ List<SpanData> exported = serviceHandler.waitForExport(1);
+ // Need to check this because otherwise the variable span1 is unused, other option is to not
+ // have a span1 variable.
+ assertThat(exported).doesNotContain(span1.toSpanData());
+ assertThat(exported).containsExactly(span2.toSpanData());
+ }
+
+ @Test(timeout = 10000L)
+ public void exportNotSampledSpansFlushed() {
+ // Set the export delay to zero, for no timeout, in order to confirm the #flush() below works
+ SpanExporterImpl spanExporter = SpanExporterImpl.create(4, Duration.create(0, 0));
+ StartEndHandler startEndHandler =
+ new StartEndHandlerImpl(spanExporter, runningSpanStore, null, new SimpleEventQueue());
+
+ spanExporter.registerHandler("test.service", serviceHandler);
+
+ RecordEventsSpanImpl span2 = createSampledEndedSpan(startEndHandler, SPAN_NAME_2);
+
+ // Force a flush, without this, the #waitForExport() call below would block indefinitely.
+ spanExporter.flush();
+
+ List<SpanData> exported = serviceHandler.waitForExport(1);
+
+ assertThat(exported).containsExactly(span2.toSpanData());
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveListTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveListTest.java
new file mode 100644
index 00000000..d7ac2ae8
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/internal/ConcurrentIntrusiveListTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.trace.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.trace.internal.ConcurrentIntrusiveList.Element;
+import javax.annotation.Nullable;
+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 ConcurrentIntrusiveList}. */
+@RunWith(JUnit4.class)
+public class ConcurrentIntrusiveListTest {
+ private final ConcurrentIntrusiveList<FakeElement> intrusiveList =
+ new ConcurrentIntrusiveList<FakeElement>();
+ @Rule public final ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void emptyList() {
+ assertThat(intrusiveList.size()).isEqualTo(0);
+ assertThat(intrusiveList.getAll().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void addRemoveAdd_SameElement() {
+ FakeElement element = new FakeElement();
+ intrusiveList.addElement(element);
+ assertThat(intrusiveList.size()).isEqualTo(1);
+ intrusiveList.removeElement(element);
+ assertThat(intrusiveList.size()).isEqualTo(0);
+ intrusiveList.addElement(element);
+ assertThat(intrusiveList.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void addAndRemoveElements() {
+ FakeElement element1 = new FakeElement();
+ FakeElement element2 = new FakeElement();
+ FakeElement element3 = new FakeElement();
+ intrusiveList.addElement(element1);
+ intrusiveList.addElement(element2);
+ intrusiveList.addElement(element3);
+ assertThat(intrusiveList.size()).isEqualTo(3);
+ assertThat(intrusiveList.getAll()).containsExactly(element3, element2, element1).inOrder();
+ // Remove element from the middle of the list.
+ intrusiveList.removeElement(element2);
+ assertThat(intrusiveList.size()).isEqualTo(2);
+ assertThat(intrusiveList.getAll()).containsExactly(element3, element1).inOrder();
+ // Remove element from the tail of the list.
+ intrusiveList.removeElement(element1);
+ assertThat(intrusiveList.size()).isEqualTo(1);
+ assertThat(intrusiveList.getAll().contains(element3)).isTrue();
+ intrusiveList.addElement(element1);
+ assertThat(intrusiveList.size()).isEqualTo(2);
+ assertThat(intrusiveList.getAll()).containsExactly(element1, element3).inOrder();
+ // Remove element from the head of the list when there are other elements after.
+ intrusiveList.removeElement(element1);
+ assertThat(intrusiveList.size()).isEqualTo(1);
+ assertThat(intrusiveList.getAll().contains(element3)).isTrue();
+ // Remove element from the head of the list when no more other elements in the list.
+ intrusiveList.removeElement(element3);
+ assertThat(intrusiveList.size()).isEqualTo(0);
+ assertThat(intrusiveList.getAll().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void addAlreadyAddedElement() {
+ FakeElement element = new FakeElement();
+ intrusiveList.addElement(element);
+ exception.expect(IllegalArgumentException.class);
+ intrusiveList.addElement(element);
+ }
+
+ @Test
+ public void removeNotAddedElement() {
+ FakeElement element = new FakeElement();
+ exception.expect(IllegalArgumentException.class);
+ intrusiveList.removeElement(element);
+ }
+
+ private static final class FakeElement implements Element<FakeElement> {
+ @Nullable private FakeElement next = null;
+ @Nullable private FakeElement prev = null;
+
+ @Override
+ public FakeElement getNext() {
+ return next;
+ }
+
+ @Override
+ public void setNext(FakeElement element) {
+ next = element;
+ }
+
+ @Override
+ public FakeElement getPrev() {
+ return prev;
+ }
+
+ @Override
+ public void setPrev(FakeElement element) {
+ prev = element;
+ }
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/B3FormatTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/B3FormatTest.java
new file mode 100644
index 00000000..52e6bb3c
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/B3FormatTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_FLAGS;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_PARENT_SPAN_ID;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_SAMPLED;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_SPAN_ID;
+import static io.opencensus.implcore.trace.propagation.B3Format.X_B3_TRACE_ID;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.propagation.SpanContextParseException;
+import io.opencensus.trace.propagation.TextFormat.Getter;
+import io.opencensus.trace.propagation.TextFormat.Setter;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+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 B3Format}. */
+@RunWith(JUnit4.class)
+public class B3FormatTest {
+ private static final String TRACE_ID_BASE16 = "ff000000000000000000000000000041";
+ private static final TraceId TRACE_ID = TraceId.fromLowerBase16(TRACE_ID_BASE16);
+ private static final String TRACE_ID_BASE16_EIGHT_BYTES = "0000000000000041";
+ private static final TraceId TRACE_ID_EIGHT_BYTES =
+ TraceId.fromLowerBase16("0000000000000000" + TRACE_ID_BASE16_EIGHT_BYTES);
+ private static final String SPAN_ID_BASE16 = "ff00000000000041";
+ private static final SpanId SPAN_ID = SpanId.fromLowerBase16(SPAN_ID_BASE16);
+ private static final byte TRACE_OPTIONS_BYTE = 1;
+ private static final TraceOptions TRACE_OPTIONS = TraceOptions.fromByte(TRACE_OPTIONS_BYTE);
+ private static final Setter<Map<String, String>> setter =
+ new Setter<Map<String, String>>() {
+ @Override
+ public void put(Map<String, String> carrier, String key, String value) {
+ carrier.put(key, value);
+ }
+ };
+ private static final Getter<Map<String, String>> getter =
+ new Getter<Map<String, String>>() {
+ @Nullable
+ @Override
+ public String get(Map<String, String> carrier, String key) {
+ return carrier.get(key);
+ }
+ };
+ private final B3Format b3Format = new B3Format();
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void serialize_SampledContext() {
+ Map<String, String> carrier = new HashMap<String, String>();
+ b3Format.inject(SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS), carrier, setter);
+ assertThat(carrier)
+ .containsExactly(
+ X_B3_TRACE_ID, TRACE_ID_BASE16, X_B3_SPAN_ID, SPAN_ID_BASE16, X_B3_SAMPLED, "1");
+ }
+
+ @Test
+ public void serialize_NotSampledContext() {
+ Map<String, String> carrier = new HashMap<String, String>();
+ b3Format.inject(SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT), carrier, setter);
+ assertThat(carrier)
+ .containsExactly(X_B3_TRACE_ID, TRACE_ID_BASE16, X_B3_SPAN_ID, SPAN_ID_BASE16);
+ }
+
+ @Test
+ public void parseMissingSampledAndMissingFlag() throws SpanContextParseException {
+ Map<String, String> headersNotSampled = new HashMap<String, String>();
+ headersNotSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+ headersNotSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ SpanContext spanContext = SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT);
+ assertThat(b3Format.extract(headersNotSampled, getter)).isEqualTo(spanContext);
+ }
+
+ @Test
+ public void parseSampled() throws SpanContextParseException {
+ Map<String, String> headersSampled = new HashMap<String, String>();
+ headersSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+ headersSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ headersSampled.put(X_B3_SAMPLED, "1");
+ assertThat(b3Format.extract(headersSampled, getter))
+ .isEqualTo(SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS));
+ }
+
+ @Test
+ public void parseZeroSampled() throws SpanContextParseException {
+ Map<String, String> headersNotSampled = new HashMap<String, String>();
+ headersNotSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+ headersNotSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ headersNotSampled.put(X_B3_SAMPLED, "0");
+ assertThat(b3Format.extract(headersNotSampled, getter))
+ .isEqualTo(SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT));
+ }
+
+ @Test
+ public void parseFlag() throws SpanContextParseException {
+ Map<String, String> headersFlagSampled = new HashMap<String, String>();
+ headersFlagSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+ headersFlagSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ headersFlagSampled.put(X_B3_FLAGS, "1");
+ assertThat(b3Format.extract(headersFlagSampled, getter))
+ .isEqualTo(SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS));
+ }
+
+ @Test
+ public void parseZeroFlag() throws SpanContextParseException {
+ Map<String, String> headersFlagNotSampled = new HashMap<String, String>();
+ headersFlagNotSampled.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+ headersFlagNotSampled.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ headersFlagNotSampled.put(X_B3_FLAGS, "0");
+ assertThat(b3Format.extract(headersFlagNotSampled, getter))
+ .isEqualTo(SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT));
+ }
+
+ @Test
+ public void parseEightBytesTraceId() throws SpanContextParseException {
+ Map<String, String> headersEightBytes = new HashMap<String, String>();
+ headersEightBytes.put(X_B3_TRACE_ID, TRACE_ID_BASE16_EIGHT_BYTES);
+ headersEightBytes.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ headersEightBytes.put(X_B3_SAMPLED, "1");
+ assertThat(b3Format.extract(headersEightBytes, getter))
+ .isEqualTo(SpanContext.create(TRACE_ID_EIGHT_BYTES, SPAN_ID, TRACE_OPTIONS));
+ }
+
+ @Test
+ public void parseEightBytesTraceId_NotSampledSpanContext() throws SpanContextParseException {
+ Map<String, String> headersEightBytes = new HashMap<String, String>();
+ headersEightBytes.put(X_B3_TRACE_ID, TRACE_ID_BASE16_EIGHT_BYTES);
+ headersEightBytes.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ assertThat(b3Format.extract(headersEightBytes, getter))
+ .isEqualTo(SpanContext.create(TRACE_ID_EIGHT_BYTES, SPAN_ID, TraceOptions.DEFAULT));
+ }
+
+ @Test
+ public void parseInvalidTraceId() throws SpanContextParseException {
+ Map<String, String> invalidHeaders = new HashMap<String, String>();
+ invalidHeaders.put(X_B3_TRACE_ID, "abcdefghijklmnop");
+ invalidHeaders.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ thrown.expect(SpanContextParseException.class);
+ thrown.expectMessage("Invalid input.");
+ b3Format.extract(invalidHeaders, getter);
+ }
+
+ @Test
+ public void parseInvalidTraceId_Size() throws SpanContextParseException {
+ Map<String, String> invalidHeaders = new HashMap<String, String>();
+ invalidHeaders.put(X_B3_TRACE_ID, "0123456789abcdef00");
+ invalidHeaders.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ thrown.expect(SpanContextParseException.class);
+ thrown.expectMessage("Invalid input.");
+ b3Format.extract(invalidHeaders, getter);
+ }
+
+ @Test
+ public void parseMissingTraceId() throws SpanContextParseException {
+ Map<String, String> invalidHeaders = new HashMap<String, String>();
+ invalidHeaders.put(X_B3_SPAN_ID, SPAN_ID_BASE16);
+ thrown.expect(SpanContextParseException.class);
+ thrown.expectMessage("Missing X_B3_TRACE_ID.");
+ b3Format.extract(invalidHeaders, getter);
+ }
+
+ @Test
+ public void parseInvalidSpanId() throws SpanContextParseException {
+ Map<String, String> invalidHeaders = new HashMap<String, String>();
+ invalidHeaders.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+ invalidHeaders.put(X_B3_SPAN_ID, "abcdefghijklmnop");
+ thrown.expect(SpanContextParseException.class);
+ thrown.expectMessage("Invalid input.");
+ b3Format.extract(invalidHeaders, getter);
+ }
+
+ @Test
+ public void parseInvalidSpanId_Size() throws SpanContextParseException {
+ Map<String, String> invalidHeaders = new HashMap<String, String>();
+ invalidHeaders.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+ invalidHeaders.put(X_B3_SPAN_ID, "0123456789abcdef00");
+ thrown.expect(SpanContextParseException.class);
+ thrown.expectMessage("Invalid input.");
+ b3Format.extract(invalidHeaders, getter);
+ }
+
+ @Test
+ public void parseMissingSpanId() throws SpanContextParseException {
+ Map<String, String> invalidHeaders = new HashMap<String, String>();
+ invalidHeaders.put(X_B3_TRACE_ID, TRACE_ID_BASE16);
+ thrown.expect(SpanContextParseException.class);
+ thrown.expectMessage("Missing X_B3_SPAN_ID.");
+ b3Format.extract(invalidHeaders, getter);
+ }
+
+ @Test
+ public void fields_list() {
+ assertThat(b3Format.fields())
+ .containsExactly(
+ X_B3_TRACE_ID, X_B3_SPAN_ID, X_B3_PARENT_SPAN_ID, X_B3_SAMPLED, X_B3_FLAGS);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplTest.java
new file mode 100644
index 00000000..f43be479
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/BinaryFormatImplTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import io.opencensus.trace.SpanContext;
+import io.opencensus.trace.SpanId;
+import io.opencensus.trace.TraceId;
+import io.opencensus.trace.TraceOptions;
+import io.opencensus.trace.propagation.BinaryFormat;
+import io.opencensus.trace.propagation.SpanContextParseException;
+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 BinaryFormatImpl}. */
+@RunWith(JUnit4.class)
+public class BinaryFormatImplTest {
+ private static final byte[] TRACE_ID_BYTES =
+ new byte[] {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
+ private static final TraceId TRACE_ID = TraceId.fromBytes(TRACE_ID_BYTES);
+ private static final byte[] SPAN_ID_BYTES = new byte[] {97, 98, 99, 100, 101, 102, 103, 104};
+ private static final SpanId SPAN_ID = SpanId.fromBytes(SPAN_ID_BYTES);
+ private static final byte TRACE_OPTIONS_BYTES = 1;
+ private static final TraceOptions TRACE_OPTIONS = TraceOptions.fromByte(TRACE_OPTIONS_BYTES);
+ private static final byte[] EXAMPLE_BYTES =
+ new byte[] {
+ 0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100,
+ 101, 102, 103, 104, 2, 1
+ };
+ private static final SpanContext EXAMPLE_SPAN_CONTEXT =
+ SpanContext.create(TRACE_ID, SPAN_ID, TRACE_OPTIONS);
+ @Rule public ExpectedException expectedException = ExpectedException.none();
+ private final BinaryFormat binaryFormat = new BinaryFormatImpl();
+
+ private void testSpanContextConversion(SpanContext spanContext) throws SpanContextParseException {
+ SpanContext propagatedBinarySpanContext =
+ binaryFormat.fromByteArray(binaryFormat.toByteArray(spanContext));
+
+ assertWithMessage("BinaryFormat propagated context is not equal with the initial context.")
+ .that(propagatedBinarySpanContext)
+ .isEqualTo(spanContext);
+ }
+
+ @Test
+ public void propagate_SpanContextTracingEnabled() throws SpanContextParseException {
+ testSpanContextConversion(
+ SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.builder().setIsSampled(true).build()));
+ }
+
+ @Test
+ public void propagate_SpanContextNoTracing() throws SpanContextParseException {
+ testSpanContextConversion(SpanContext.create(TRACE_ID, SPAN_ID, TraceOptions.DEFAULT));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void toBinaryValue_NullSpanContext() {
+ binaryFormat.toByteArray(null);
+ }
+
+ @Test
+ public void toBinaryValue_InvalidSpanContext() {
+ assertThat(binaryFormat.toByteArray(SpanContext.INVALID))
+ .isEqualTo(
+ new byte[] {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0
+ });
+ }
+
+ @Test
+ public void fromBinaryValue_BinaryExampleValue() throws SpanContextParseException {
+ assertThat(binaryFormat.fromByteArray(EXAMPLE_BYTES)).isEqualTo(EXAMPLE_SPAN_CONTEXT);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void fromBinaryValue_NullInput() throws SpanContextParseException {
+ binaryFormat.fromByteArray(null);
+ }
+
+ @Test
+ public void fromBinaryValue_EmptyInput() throws SpanContextParseException {
+ expectedException.expect(SpanContextParseException.class);
+ expectedException.expectMessage("Unsupported version.");
+ binaryFormat.fromByteArray(new byte[0]);
+ }
+
+ @Test
+ public void fromBinaryValue_UnsupportedVersionId() throws SpanContextParseException {
+ expectedException.expect(SpanContextParseException.class);
+ expectedException.expectMessage("Unsupported version.");
+ binaryFormat.fromByteArray(
+ new byte[] {
+ 66, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 97, 98, 99, 100, 101,
+ 102, 103, 104, 1
+ });
+ }
+
+ @Test
+ public void fromBinaryValue_UnsupportedFieldIdFirst() throws SpanContextParseException {
+ expectedException.expect(SpanContextParseException.class);
+ expectedException.expectMessage(
+ "Invalid input: expected trace ID at offset " + BinaryFormatImpl.TRACE_ID_FIELD_ID_OFFSET);
+ binaryFormat.fromByteArray(
+ new byte[] {
+ 0, 4, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100,
+ 101, 102, 103, 104, 2, 1
+ });
+ }
+
+ @Test
+ public void fromBinaryValue_UnsupportedFieldIdSecond() throws SpanContextParseException {
+ expectedException.expect(SpanContextParseException.class);
+ expectedException.expectMessage(
+ "Invalid input: expected span ID at offset " + BinaryFormatImpl.SPAN_ID_FIELD_ID_OFFSET);
+ binaryFormat.fromByteArray(
+ new byte[] {
+ 0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 3, 97, 98, 99, 100,
+ 101, 102, 103, 104, 2, 1
+ });
+ }
+
+ @Test
+ public void fromBinaryValue_UnsupportedFieldIdThird_skipped() throws SpanContextParseException {
+ assertThat(
+ binaryFormat
+ .fromByteArray(
+ new byte[] {
+ 0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97,
+ 98, 99, 100, 101, 102, 103, 104, 0, 1
+ })
+ .isValid())
+ .isTrue();
+ }
+
+ @Test
+ public void fromBinaryValue_ShorterTraceId() throws SpanContextParseException {
+ expectedException.expect(SpanContextParseException.class);
+ expectedException.expectMessage("Invalid input: truncated");
+ binaryFormat.fromByteArray(
+ new byte[] {0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76});
+ }
+
+ @Test
+ public void fromBinaryValue_ShorterSpanId() throws SpanContextParseException {
+ expectedException.expect(SpanContextParseException.class);
+ expectedException.expectMessage("Invalid input: truncated");
+ binaryFormat.fromByteArray(new byte[] {0, 1, 97, 98, 99, 100, 101, 102, 103});
+ }
+
+ @Test
+ public void fromBinaryValue_ShorterTraceOptions() throws SpanContextParseException {
+ expectedException.expect(SpanContextParseException.class);
+ expectedException.expectMessage("Invalid input: truncated");
+ binaryFormat.fromByteArray(
+ new byte[] {
+ 0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99, 100,
+ 101, 102, 103, 104, 2
+ });
+ }
+
+ @Test
+ public void fromBinaryValue_MissingTraceOptionsOk() throws SpanContextParseException {
+ SpanContext extracted =
+ binaryFormat.fromByteArray(
+ new byte[] {
+ 0, 0, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 1, 97, 98, 99,
+ 100, 101, 102, 103, 104
+ });
+
+ assertThat(extracted.isValid()).isTrue();
+ assertThat(extracted.getTraceOptions()).isEqualTo(TraceOptions.DEFAULT);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/PropagationComponentImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/PropagationComponentImplTest.java
new file mode 100644
index 00000000..00ed90fe
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/trace/propagation/PropagationComponentImplTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.trace.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.trace.propagation.PropagationComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link PropagationComponentImpl}. */
+@RunWith(JUnit4.class)
+public class PropagationComponentImplTest {
+ private final PropagationComponent propagationComponent = new PropagationComponentImpl();
+
+ @Test
+ public void implementationOfBinary() {
+ assertThat(propagationComponent.getBinaryFormat()).isInstanceOf(BinaryFormatImpl.class);
+ }
+
+ @Test
+ public void implementationOfB3Format() {
+ assertThat(propagationComponent.getB3Format()).isInstanceOf(B3Format.class);
+ }
+}
diff --git a/impl_lite/README.md b/impl_lite/README.md
new file mode 100644
index 00000000..ad7bb9b1
--- /dev/null
+++ b/impl_lite/README.md
@@ -0,0 +1,6 @@
+OpenCensus Android implementation
+======================================================
+
+* Android compatible.
+* StatsManager specifies the stats implementation classes that should be used
+ with Android.
diff --git a/impl_lite/build.gradle b/impl_lite/build.gradle
new file mode 100644
index 00000000..b8692fdf
--- /dev/null
+++ b/impl_lite/build.gradle
@@ -0,0 +1,12 @@
+description = 'OpenCensus Lite Implementation'
+
+dependencies {
+ compile project(':opencensus-api'),
+ project(':opencensus-impl-core')
+
+ testCompile project(':opencensus-api'),
+ project(':opencensus-impl-core')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/impl_lite/src/main/java/io/opencensus/impllite/metrics/MetricsComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/impllite/metrics/MetricsComponentImplLite.java
new file mode 100644
index 00000000..6161c12a
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/impllite/metrics/MetricsComponentImplLite.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2018, 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.impllite.metrics;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.metrics.MetricsComponentImplBase;
+import io.opencensus.metrics.MetricsComponent;
+
+/** Android-compatible implementation of {@link MetricsComponent}. */
+public final class MetricsComponentImplLite extends MetricsComponentImplBase {
+
+ public MetricsComponentImplLite() {
+ super(MillisClock.getInstance());
+ }
+}
diff --git a/impl_lite/src/main/java/io/opencensus/impllite/stats/StatsComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/impllite/stats/StatsComponentImplLite.java
new file mode 100644
index 00000000..a58a9d3e
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/impllite/stats/StatsComponentImplLite.java
@@ -0,0 +1,31 @@
+/*
+ * 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.impllite.stats;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.stats.StatsComponentImplBase;
+import io.opencensus.stats.StatsComponent;
+
+/** Android-compatible implementation of {@link StatsComponent}. */
+public final class StatsComponentImplLite extends StatsComponentImplBase {
+
+ public StatsComponentImplLite() {
+ // TODO(sebright): Use a more efficient queue implementation.
+ super(new SimpleEventQueue(), MillisClock.getInstance());
+ }
+}
diff --git a/impl_lite/src/main/java/io/opencensus/impllite/tags/TagsComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/impllite/tags/TagsComponentImplLite.java
new file mode 100644
index 00000000..dc0d900c
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/impllite/tags/TagsComponentImplLite.java
@@ -0,0 +1,23 @@
+/*
+ * 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.impllite.tags;
+
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.TagsComponent;
+
+/** Android-compatible implementation of {@link TagsComponent}. */
+public final class TagsComponentImplLite extends TagsComponentImplBase {}
diff --git a/impl_lite/src/main/java/io/opencensus/impllite/trace/TraceComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/impllite/trace/TraceComponentImplLite.java
new file mode 100644
index 00000000..8c067557
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/impllite/trace/TraceComponentImplLite.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.impllite.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.TraceComponentImplBase;
+import io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler;
+import io.opencensus.trace.TraceComponent;
+import io.opencensus.trace.Tracer;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/** Android-compatible implementation of the {@link TraceComponent}. */
+public final class TraceComponentImplLite extends TraceComponent {
+ private final TraceComponentImplBase traceComponentImplBase;
+
+ /** Public constructor to be used with reflection loading. */
+ public TraceComponentImplLite() {
+ traceComponentImplBase =
+ new TraceComponentImplBase(
+ MillisClock.getInstance(), new SecureRandomHandler(), new SimpleEventQueue());
+ }
+
+ @Override
+ public Tracer getTracer() {
+ return traceComponentImplBase.getTracer();
+ }
+
+ @Override
+ public PropagationComponent getPropagationComponent() {
+ return traceComponentImplBase.getPropagationComponent();
+ }
+
+ @Override
+ public Clock getClock() {
+ return traceComponentImplBase.getClock();
+ }
+
+ @Override
+ public ExportComponent getExportComponent() {
+ return traceComponentImplBase.getExportComponent();
+ }
+
+ @Override
+ public TraceConfig getTraceConfig() {
+ return traceComponentImplBase.getTraceConfig();
+ }
+}
diff --git a/impl_lite/src/main/java/io/opencensus/trace/TraceComponentImplLite.java b/impl_lite/src/main/java/io/opencensus/trace/TraceComponentImplLite.java
new file mode 100644
index 00000000..5e80b93a
--- /dev/null
+++ b/impl_lite/src/main/java/io/opencensus/trace/TraceComponentImplLite.java
@@ -0,0 +1,66 @@
+/*
+ * 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.trace;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.trace.TraceComponentImplBase;
+import io.opencensus.implcore.trace.internal.RandomHandler.SecureRandomHandler;
+import io.opencensus.trace.config.TraceConfig;
+import io.opencensus.trace.export.ExportComponent;
+import io.opencensus.trace.propagation.PropagationComponent;
+
+/** Android-compatible implementation of the {@link TraceComponent}. */
+// TraceComponentImplLite was moved to io.opencensus.impllite.trace. This class exists for backwards
+// compatibility, so that it can be loaded by opencensus-api 0.5.
+@Deprecated
+public final class TraceComponentImplLite extends TraceComponent {
+ private final TraceComponentImplBase traceComponentImplBase;
+
+ /** Public constructor to be used with reflection loading. */
+ public TraceComponentImplLite() {
+ traceComponentImplBase =
+ new TraceComponentImplBase(
+ MillisClock.getInstance(), new SecureRandomHandler(), new SimpleEventQueue());
+ }
+
+ @Override
+ public Tracer getTracer() {
+ return traceComponentImplBase.getTracer();
+ }
+
+ @Override
+ public PropagationComponent getPropagationComponent() {
+ return traceComponentImplBase.getPropagationComponent();
+ }
+
+ @Override
+ public Clock getClock() {
+ return traceComponentImplBase.getClock();
+ }
+
+ @Override
+ public ExportComponent getExportComponent() {
+ return traceComponentImplBase.getExportComponent();
+ }
+
+ @Override
+ public TraceConfig getTraceConfig() {
+ return traceComponentImplBase.getTraceConfig();
+ }
+}
diff --git a/impl_lite/src/test/java/io/opencensus/impllite/metrics/MetricsTest.java b/impl_lite/src/test/java/io/opencensus/impllite/metrics/MetricsTest.java
new file mode 100644
index 00000000..7ee900a6
--- /dev/null
+++ b/impl_lite/src/test/java/io/opencensus/impllite/metrics/MetricsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018, 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.impllite.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.metrics.MetricRegistryImpl;
+import io.opencensus.implcore.metrics.export.ExportComponentImpl;
+import io.opencensus.metrics.Metrics;
+import io.opencensus.metrics.MetricsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link MetricsComponent} through the {@link Metrics} class. */
+@RunWith(JUnit4.class)
+public class MetricsTest {
+
+ @Test
+ public void getExportComponent() {
+ assertThat(Metrics.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+ }
+
+ @Test
+ public void getMetricRegistry() {
+ assertThat(Metrics.getMetricRegistry()).isInstanceOf(MetricRegistryImpl.class);
+ }
+}
diff --git a/impl_lite/src/test/java/io/opencensus/impllite/stats/StatsTest.java b/impl_lite/src/test/java/io/opencensus/impllite/stats/StatsTest.java
new file mode 100644
index 00000000..313f8916
--- /dev/null
+++ b/impl_lite/src/test/java/io/opencensus/impllite/stats/StatsTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.impllite.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.stats.StatsRecorderImpl;
+import io.opencensus.implcore.stats.ViewManagerImpl;
+import io.opencensus.stats.Stats;
+import io.opencensus.stats.StatsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link StatsComponent} through the {@link Stats} class. */
+@RunWith(JUnit4.class)
+public final class StatsTest {
+ @Test
+ public void getStatsRecorder() {
+ assertThat(Stats.getStatsRecorder()).isInstanceOf(StatsRecorderImpl.class);
+ }
+
+ @Test
+ public void getViewManager() {
+ assertThat(Stats.getViewManager()).isInstanceOf(ViewManagerImpl.class);
+ }
+}
diff --git a/impl_lite/src/test/java/io/opencensus/impllite/tags/TagsTest.java b/impl_lite/src/test/java/io/opencensus/impllite/tags/TagsTest.java
new file mode 100644
index 00000000..890cdb15
--- /dev/null
+++ b/impl_lite/src/test/java/io/opencensus/impllite/tags/TagsTest.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.impllite.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.tags.TaggerImpl;
+import io.opencensus.implcore.tags.propagation.TagPropagationComponentImpl;
+import io.opencensus.tags.Tags;
+import io.opencensus.tags.TagsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Test for accessing the {@link TagsComponent} through the {@link Tags} class. */
+@RunWith(JUnit4.class)
+public final class TagsTest {
+ @Test
+ public void getTagger() {
+ assertThat(Tags.getTagger()).isInstanceOf(TaggerImpl.class);
+ }
+
+ @Test
+ public void getTagContextSerializer() {
+ assertThat(Tags.getTagPropagationComponent()).isInstanceOf(TagPropagationComponentImpl.class);
+ }
+}
diff --git a/impl_lite/src/test/java/io/opencensus/impllite/trace/TraceComponentImplLiteTest.java b/impl_lite/src/test/java/io/opencensus/impllite/trace/TraceComponentImplLiteTest.java
new file mode 100644
index 00000000..c4a609a4
--- /dev/null
+++ b/impl_lite/src/test/java/io/opencensus/impllite/trace/TraceComponentImplLiteTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.impllite.trace;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.common.MillisClock;
+import io.opencensus.implcore.trace.TracerImpl;
+import io.opencensus.implcore.trace.export.ExportComponentImpl;
+import io.opencensus.implcore.trace.propagation.PropagationComponentImpl;
+import io.opencensus.trace.Tracing;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link TraceComponentImplLite}. */
+@RunWith(JUnit4.class)
+public class TraceComponentImplLiteTest {
+ @Test
+ public void implementationOfTracer() {
+ assertThat(Tracing.getTracer()).isInstanceOf(TracerImpl.class);
+ }
+
+ @Test
+ public void implementationOfBinaryPropagationHandler() {
+ assertThat(Tracing.getPropagationComponent()).isInstanceOf(PropagationComponentImpl.class);
+ }
+
+ @Test
+ public void implementationOfClock() {
+ assertThat(Tracing.getClock()).isInstanceOf(MillisClock.class);
+ }
+
+ @Test
+ public void implementationOfTraceExporter() {
+ assertThat(Tracing.getExportComponent()).isInstanceOf(ExportComponentImpl.class);
+ }
+}
diff --git a/scripts/check-git-history.py b/scripts/check-git-history.py
new file mode 100644
index 00000000..df13e423
--- /dev/null
+++ b/scripts/check-git-history.py
@@ -0,0 +1,51 @@
+import os
+import sys
+import traceback
+
+def main(argv):
+ # Only check the history if the build is running on a pull request.
+ # Build could be running on pull request using travis or kokoro.
+ if is_travis_pull_request() or is_kokoro_presubmit_request():
+ # This function assumes that HEAD^1 is the base branch and HEAD^2 is the
+ # pull request.
+ exit_if_pull_request_has_merge_commits()
+ print 'Checked pull request history: SUCCEEDED'
+ else:
+ print 'Skipped history check.'
+
+def is_kokoro_presubmit_request():
+ '''Returns true if KOKORO_GITHUB_PULL_REQUEST_NUMBER is set.'''
+ if 'KOKORO_GITHUB_PULL_REQUEST_NUMBER' in os.environ:
+ return True
+ return False
+
+def is_travis_pull_request():
+ '''Returns true if TRAVIS_PULL_REQUEST is set to indicate a pull request.'''
+ if 'TRAVIS_PULL_REQUEST' in os.environ:
+ return os.environ['TRAVIS_PULL_REQUEST'] != 'false'
+ return False
+
+def exit_if_pull_request_has_merge_commits():
+ '''Exits with an error if any of the commits added by the pull request are
+ merge commits.'''
+ # Print the parents of each commit added by the pull request.
+ git_command = 'git log --format="%P" HEAD^1..HEAD^2'
+ for line in os.popen(git_command):
+ parents = line.split()
+ assert len(parents) >= 1, line
+ if len(parents) > 1:
+ print 'Pull request contains a merge commit:'
+ print_history()
+ print 'Checked pull request history: FAILED'
+ sys.exit(1)
+
+def print_history():
+ os.system('git log HEAD^1 HEAD^2 -30 --graph --oneline --decorate')
+
+def read_process(command):
+ '''Runs a command and returns everything printed to stdout.'''
+ with os.popen(command, 'r') as fd:
+ return fd.read()
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/scripts/travis_script b/scripts/travis_script
new file mode 100755
index 00000000..7b7bec50
--- /dev/null
+++ b/scripts/travis_script
@@ -0,0 +1,78 @@
+#!/bin/bash
+#
+# Travis build script, cf.
+# https://docs.travis-ci.com/user/customizing-the-build/#Implementing-Complex-Build-Steps.
+
+set -o errexit
+set -o xtrace
+
+case "$TASK" in
+ "CHECK_GIT_HISTORY")
+ python "$(dirname "$0")"/check-git-history.py
+ ;;
+ "BUILD")
+ case "$TRAVIS_OS_NAME" in
+ "linux")
+ source /opt/jdk_switcher/jdk_switcher.sh
+ export JAVA8_HOME="$(jdk_switcher home oraclejdk8)"
+ case "$TRAVIS_JDK_VERSION" in
+ "oraclejdk9")
+ ./gradlew clean assemble check --stacktrace
+ ;;
+ "oraclejdk8")
+ export JAVA_HOMES="$(jdk_switcher home openjdk6)/jre:$(jdk_switcher home openjdk7)/jre:$(jdk_switcher home oraclejdk8)/jre:$(jdk_switcher home oraclejdk9)"
+ ./gradlew clean assemble --stacktrace
+ ./gradlew check :opencensus-all:jacocoTestReport
+ ./gradlew verGJF
+ ;;
+ "openjdk7")
+ # "./gradlew classes testClasses" is a workaround for
+ # https://github.com/gradle/gradle/issues/2421.
+ # See https://github.com/gradle/gradle/issues/2421#issuecomment-319916874.
+ JAVA_HOME="$(jdk_switcher home openjdk8)" ./gradlew classes testClasses
+ ./gradlew clean assemble --stacktrace
+ ./gradlew check
+ ;;
+ *)
+ echo "Unknown JDK version $TRAVIS_JDK_VERSION"
+ exit 1
+ ;;
+ esac
+ ;;
+ "osx")
+ # OS X is a separate case, because the JDK version is determined by the OS X image:
+ # https://docs.travis-ci.com/user/reference/osx/#JDK-and-OS-X
+ ./gradlew clean assemble --stacktrace
+ ./gradlew check
+ ;;
+ *)
+ echo "Unknown OS name $TRAVIS_OS_NAME"
+ exit 1
+ ;;
+ esac
+ ;;
+ "CHECKER_FRAMEWORK")
+ ./gradlew clean assemble -PcheckerFramework=true
+ ;;
+ "CHECK_EXAMPLES_LICENSE")
+ curl -L -o checkstyle-8.12-all.jar https://github.com/checkstyle/checkstyle/releases/download/checkstyle-8.12/checkstyle-8.12-all.jar
+ java -DrootDir=. -jar checkstyle-8.12-all.jar -c buildscripts/checkstyle.xml examples/src/
+ ;;
+ "CHECK_EXAMPLES_FORMAT")
+ curl -L -o google-java-format-1.5-all-deps.jar https://github.com/google/google-java-format/releases/download/google-java-format-1.5/google-java-format-1.5-all-deps.jar
+ java -jar google-java-format-1.5-all-deps.jar --set-exit-if-changed --dry-run `find examples/src/ -name '*.java'`
+ ;;
+ "BUILD_EXAMPLES_GRADLE")
+ pushd examples && ./gradlew clean assemble --stacktrace && popd
+ ;;
+ "BUILD_EXAMPLES_MAVEN")
+ pushd examples && mvn clean package appassembler:assemble -e && popd
+ ;;
+ "BUILD_EXAMPLES_BAZEL")
+ pushd examples && bazel clean && bazel build :all && popd
+ ;;
+ *)
+ echo "Unknown task $TASK"
+ exit 1
+ ;;
+esac
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..7c224edf
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,79 @@
+rootProject.name = "opencensus-java"
+
+include ":opencensus-api"
+include ":opencensus-impl-core"
+include ":opencensus-impl-lite"
+include ":opencensus-impl"
+include ":opencensus-testing"
+include ":opencensus-exporter-trace-instana"
+include ":opencensus-exporter-trace-logging"
+include ":opencensus-exporter-trace-ocagent"
+include ":opencensus-exporter-trace-stackdriver"
+include ":opencensus-exporter-trace-zipkin"
+include ":opencensus-exporter-trace-jaeger"
+include ":opencensus-exporter-stats-signalfx"
+include ":opencensus-exporter-stats-stackdriver"
+include ":opencensus-exporter-stats-prometheus"
+include ":opencensus-contrib-agent"
+include ":opencensus-contrib-appengine-standard-util"
+include ":opencensus-contrib-dropwizard"
+include ":opencensus-contrib-exemplar-util"
+include ":opencensus-contrib-grpc-metrics"
+include ":opencensus-contrib-grpc-util"
+include ":opencensus-contrib-http-util"
+include ":opencensus-contrib-log-correlation-log4j2"
+include ":opencensus-contrib-log-correlation-stackdriver"
+include ":opencensus-contrib-monitored-resource-util"
+include ":opencensus-contrib-spring"
+include ":opencensus-contrib-spring-sleuth-v1x"
+
+project(':opencensus-api').projectDir = "$rootDir/api" as File
+project(':opencensus-impl-core').projectDir = "$rootDir/impl_core" as File
+project(':opencensus-impl-lite').projectDir = "$rootDir/impl_lite" as File
+project(':opencensus-impl').projectDir = "$rootDir/impl" as File
+project(':opencensus-testing').projectDir = "$rootDir/testing" as File
+project(':opencensus-contrib-agent').projectDir = "$rootDir/contrib/agent" as File
+project(':opencensus-contrib-appengine-standard-util').projectDir =
+ "$rootDir/contrib/appengine_standard_util" as File
+project(':opencensus-contrib-dropwizard').projectDir = "$rootDir/contrib/dropwizard" as File
+project(':opencensus-contrib-exemplar-util').projectDir = "$rootDir/contrib/exemplar_util" as File
+project(':opencensus-contrib-grpc-metrics').projectDir = "$rootDir/contrib/grpc_metrics" as File
+project(':opencensus-contrib-grpc-util').projectDir = "$rootDir/contrib/grpc_util" as File
+project(':opencensus-contrib-http-util').projectDir = "$rootDir/contrib/http_util" as File
+project(':opencensus-contrib-log-correlation-log4j2').projectDir =
+ "$rootDir/contrib/log_correlation/log4j2" as File
+project(':opencensus-contrib-log-correlation-stackdriver').projectDir =
+ "$rootDir/contrib/log_correlation/stackdriver" as File
+project(':opencensus-contrib-monitored-resource-util').projectDir =
+ "$rootDir/contrib/monitored_resource_util" as File
+project(':opencensus-contrib-spring').projectDir = "$rootDir/contrib/spring" as File
+project(':opencensus-contrib-spring-sleuth-v1x').projectDir =
+ "$rootDir/contrib/spring_sleuth_v1x" as File
+project(':opencensus-exporter-stats-signalfx').projectDir =
+ "$rootDir/exporters/stats/signalfx" as File
+project(':opencensus-exporter-stats-stackdriver').projectDir =
+ "$rootDir/exporters/stats/stackdriver" as File
+project(':opencensus-exporter-stats-prometheus').projectDir =
+ "$rootDir/exporters/stats/prometheus" as File
+project(':opencensus-exporter-trace-instana').projectDir =
+ "$rootDir/exporters/trace/instana" as File
+project(':opencensus-exporter-trace-logging').projectDir =
+ "$rootDir/exporters/trace/logging" as File
+project(':opencensus-exporter-trace-ocagent').projectDir =
+ "$rootDir/exporters/trace/ocagent" as File
+project(':opencensus-exporter-trace-stackdriver').projectDir =
+ "$rootDir/exporters/trace/stackdriver" as File
+project(':opencensus-exporter-trace-zipkin').projectDir = "$rootDir/exporters/trace/zipkin" as File
+project(':opencensus-exporter-trace-jaeger').projectDir = "$rootDir/exporters/trace/jaeger" as File
+
+
+// Java8 projects only
+if (JavaVersion.current().isJava8Compatible()) {
+ include ":opencensus-all"
+ include ":opencensus-benchmarks"
+ include ":opencensus-contrib-zpages"
+
+ project(':opencensus-all').projectDir = "$rootDir/all" as File
+ project(':opencensus-benchmarks').projectDir = "$rootDir/benchmarks" as File
+ project(':opencensus-contrib-zpages').projectDir = "$rootDir/contrib/zpages" as File
+}
diff --git a/testing/README.md b/testing/README.md
new file mode 100644
index 00000000..268bbf3d
--- /dev/null
+++ b/testing/README.md
@@ -0,0 +1,5 @@
+OpenCensus Testing Package
+======================================================
+
+* Java 6 and Android compatible.
+* The classes in this directory can be used to test the API integration.
diff --git a/testing/build.gradle b/testing/build.gradle
new file mode 100644
index 00000000..811b0597
--- /dev/null
+++ b/testing/build.gradle
@@ -0,0 +1,11 @@
+description = 'OpenCensus Testing'
+
+dependencies {
+ compile project(':opencensus-api'),
+ libraries.guava
+
+ testCompile project(':opencensus-api')
+
+ signature "org.codehaus.mojo.signature:java17:1.0@signature"
+ signature "net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature"
+}
diff --git a/testing/src/main/java/io/opencensus/testing/common/TestClock.java b/testing/src/main/java/io/opencensus/testing/common/TestClock.java
new file mode 100644
index 00000000..b670cb7f
--- /dev/null
+++ b/testing/src/main/java/io/opencensus/testing/common/TestClock.java
@@ -0,0 +1,104 @@
+/*
+ * 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.testing.common;
+
+import com.google.common.math.LongMath;
+import io.opencensus.common.Clock;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * A {@link Clock} that allows the time to be set for testing.
+ *
+ * @since 0.5
+ */
+@ThreadSafe
+public final class TestClock extends Clock {
+ private static final int NUM_NANOS_PER_SECOND = 1000 * 1000 * 1000;
+
+ @GuardedBy("this")
+ private Timestamp currentTime = validateNanos(Timestamp.create(1493419949, 223123456));
+
+ private TestClock() {}
+
+ /**
+ * Creates a clock initialized to a constant non-zero time. {@code Timestamp.create(0, 0)} is not
+ * a good default, because it represents an invalid time.
+ *
+ * @return a clock initialized to a constant non-zero time.
+ * @since 0.5
+ */
+ public static TestClock create() {
+ return new TestClock();
+ }
+
+ /**
+ * Creates a clock with the given time.
+ *
+ * @param time the initial time.
+ * @return a new {@code TestClock} with the given time.
+ * @since 0.5
+ */
+ public static TestClock create(Timestamp time) {
+ TestClock clock = new TestClock();
+ clock.setTime(time);
+ return clock;
+ }
+
+ /**
+ * Sets the time.
+ *
+ * @param time the new time.
+ * @since 0.5
+ */
+ public synchronized void setTime(Timestamp time) {
+ currentTime = validateNanos(time);
+ }
+
+ /**
+ * Advances the time by a duration.
+ *
+ * @param duration the increase in time.
+ * @since 0.5
+ */
+ public synchronized void advanceTime(Duration duration) {
+ currentTime = validateNanos(currentTime.addDuration(duration));
+ }
+
+ @Override
+ public synchronized Timestamp now() {
+ return currentTime;
+ }
+
+ @Override
+ public synchronized long nowNanos() {
+ return getNanos(currentTime);
+ }
+
+ private static Timestamp validateNanos(Timestamp time) {
+ getNanos(time);
+ return time;
+ }
+
+ // Converts Timestamp into nanoseconds since time 0 and throws an exception if it overflows.
+ private static long getNanos(Timestamp time) {
+ return LongMath.checkedAdd(
+ LongMath.checkedMultiply(time.getSeconds(), NUM_NANOS_PER_SECOND), time.getNanos());
+ }
+}
diff --git a/testing/src/main/java/io/opencensus/testing/export/TestHandler.java b/testing/src/main/java/io/opencensus/testing/export/TestHandler.java
new file mode 100644
index 00000000..6d73aff1
--- /dev/null
+++ b/testing/src/main/java/io/opencensus/testing/export/TestHandler.java
@@ -0,0 +1,77 @@
+/*
+ * 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.testing.export;
+
+import io.opencensus.trace.export.SpanData;
+import io.opencensus.trace.export.SpanExporter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * A {@link SpanExporter.Handler} for testing only.
+ *
+ * @since 0.9
+ */
+public final class TestHandler extends SpanExporter.Handler {
+
+ private final Object monitor = new Object();
+
+ // TODO: Decide whether to use a different class instead of LinkedList.
+ @GuardedBy("monitor")
+ @SuppressWarnings("JdkObsolete")
+ private final List<SpanData> spanDataList = new LinkedList<SpanData>();
+
+ @Override
+ public void export(Collection<SpanData> spanDataList) {
+ synchronized (monitor) {
+ this.spanDataList.addAll(spanDataList);
+ monitor.notifyAll();
+ }
+ }
+
+ /**
+ * Waits until we received numberOfSpans spans to export. Returns the list of exported {@link
+ * SpanData} objects, otherwise {@code null} if the current thread is interrupted.
+ *
+ * @param numberOfSpans the number of minimum spans to be collected.
+ * @return the list of exported {@link SpanData} objects, otherwise {@code null} if the current
+ * thread is interrupted.
+ * @since 0.9
+ */
+ @Nullable
+ public List<SpanData> waitForExport(int numberOfSpans) {
+ List<SpanData> ret;
+ synchronized (monitor) {
+ while (spanDataList.size() < numberOfSpans) {
+ try {
+ monitor.wait();
+ } catch (InterruptedException e) {
+ // Preserve the interruption status as per guidance.
+ Thread.currentThread().interrupt();
+ return null;
+ }
+ }
+ ret = new ArrayList<SpanData>(spanDataList);
+ spanDataList.clear();
+ }
+ return ret;
+ }
+}
diff --git a/testing/src/test/java/io/opencensus/testing/common/TestClockTest.java b/testing/src/test/java/io/opencensus/testing/common/TestClockTest.java
new file mode 100644
index 00000000..24cd7fcd
--- /dev/null
+++ b/testing/src/test/java/io/opencensus/testing/common/TestClockTest.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.testing.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TestClock}. */
+@RunWith(JUnit4.class)
+public final class TestClockTest {
+ private static final int NUM_NANOS_PER_SECOND = 1000 * 1000 * 1000;
+
+ @Test
+ public void setAndGetTime() {
+ TestClock clock = TestClock.create(Timestamp.create(1, 2));
+ assertThat(clock.now()).isEqualTo(Timestamp.create(1, 2));
+ clock.setTime(Timestamp.create(3, 4));
+ assertThat(clock.now()).isEqualTo(Timestamp.create(3, 4));
+ }
+
+ @Test
+ public void advanceTime() {
+ TestClock clock = TestClock.create(Timestamp.create(1, 500 * 1000 * 1000));
+ clock.advanceTime(Duration.create(2, 600 * 1000 * 1000));
+ assertThat(clock.now()).isEqualTo(Timestamp.create(4, 100 * 1000 * 1000));
+ }
+
+ @Test
+ public void measureElapsedTime() {
+ TestClock clock = TestClock.create(Timestamp.create(10, 1));
+ long nanos1 = clock.nowNanos();
+ clock.setTime(Timestamp.create(11, 5));
+ long nanos2 = clock.nowNanos();
+ assertThat(nanos2 - nanos1).isEqualTo(1000 * 1000 * 1000 + 4);
+ }
+
+ @Test(expected = ArithmeticException.class)
+ public void catchOverflow() {
+ TestClock.create(Timestamp.create(Long.MAX_VALUE / NUM_NANOS_PER_SECOND + 1, 0));
+ }
+
+ @Test(expected = ArithmeticException.class)
+ public void catchNegativeOverflow() {
+ TestClock.create(Timestamp.create(Long.MIN_VALUE / NUM_NANOS_PER_SECOND - 1, 0));
+ }
+}