Repository: census-instrumentation/opencensus-go
Pull request #878 (2018-08-22 16:54:26)

Add resource package
This package adds resources as a core concept to OpenCensus.

@rghetia @bogdandrutu  @ramonza @rakyll @jkohen As this is the core step of a relatively fundamental change, you may all want to take a quick look.


ramonza: @fabxc this is an interesting idea but definitely needs to be discussed on opencensus-specs first, would you mind raising an issue or PR there for discussion?

Patch:
diff --git a/.travis.yml b/.travis.yml
index 2d6daa6..73c8571 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,7 @@ language: go
 
 go:
   # 1.8 is tested by AppVeyor
-  - 1.10.x
+  - 1.11.x
 
 go_import_path: go.opencensus.io
 
@@ -18,10 +18,10 @@ before_script:
 
 script:
   - embedmd -d README.md              # Ensure embedded code is up-to-date
-  - dep ensure -v
   - go build ./...                    # Ensure dependency updates don't break build
   - if [ -n "$(gofmt -s -l $GO_FILES)" ]; then echo "gofmt the following files:"; gofmt -s -l $GO_FILES; exit 1; fi
   - go vet ./...
   - go test -v -race $PKGS            # Run all the tests with the race detector enabled
+  - GOARCH=386 go test -v $PKGS       # Run all tests against a 386 architecture
   - 'if [[ $TRAVIS_GO_VERSION = 1.8* ]]; then ! golint ./... | grep -vE "(_mock|_string|\.pb)\.go:"; fi'
   - go run internal/check/version.go
diff --git a/README.md b/README.md
index bc50ee4..97d6698 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
 
 OpenCensus Go is a Go implementation of OpenCensus, a toolkit for
 collecting application performance and behavior monitoring data.
-Currently it consists of three major components: tags, stats, and tracing.
+Currently it consists of three major components: tags, stats and tracing.
 
 ## Installation
 
@@ -38,7 +38,7 @@ integration with your RPC framework:
 * [Redis goredis/redis](https://godoc.org/github.com/orijtech/redis)
 * [Memcache](https://godoc.org/github.com/orijtech/gomemcache)
 
-If you're a framework not listed here, you could either implement your own middleware for your
+If you're using a framework not listed here, you could either implement your own middleware for your
 framework or use [custom stats](#stats) and [spans](#spans) directly in your application.
 
 ## Exporters
@@ -55,6 +55,8 @@ can implement their own exporters by implementing the exporter interfaces
 * [Jaeger][exporter-jaeger] for traces
 * [AWS X-Ray][exporter-xray] for traces
 * [Datadog][exporter-datadog] for stats and traces
+* [Graphite][exporter-graphite] for stats
+* [Honeycomb][exporter-honeycomb] for traces
 
 ## Overview
 
@@ -72,7 +74,7 @@ in the same process or can be encoded to be transmitted on the wire. Usually, th
 be handled by an integration plugin, e.g. `ocgrpc.ServerHandler` and `ocgrpc.ClientHandler`
 for gRPC.
 
-Package tag allows adding or modifying tags in the current context.
+Package `tag` allows adding or modifying tags in the current context.
 
 [embedmd]:# (internal/readme/tags.go new)
 ```go
@@ -176,8 +178,8 @@ Spans can have parents or can be root spans if they don't have any parents.
 The current span is propagated in-process and across the network to allow associating
 new child spans with the parent.
 
-In the same process, context.Context is used to propagate spans.
-trace.StartSpan creates a new span as a root if the current context
+In the same process, `context.Context` is used to propagate spans.
+`trace.StartSpan` creates a new span as a root if the current context
 doesn't contain a span. Or, it creates a child of the span that is
 already in current context. The returned context can be used to keep
 propagating the newly created span in the current context.
@@ -193,8 +195,8 @@ defer span.End()
 Across the network, OpenCensus provides different propagation
 methods for different protocols.
 
-* gRPC integrations uses the OpenCensus' [binary propagation format](https://godoc.org/go.opencensus.io/trace/propagation).
-* HTTP integrations uses Zipkin's [B3](https://github.com/openzipkin/b3-propagation)
+* gRPC integrations use the OpenCensus' [binary propagation format](https://godoc.org/go.opencensus.io/trace/propagation).
+* HTTP integrations use Zipkin's [B3](https://github.com/openzipkin/b3-propagation)
   by default but can be configured to use a custom propagation method by setting another
   [propagation.HTTPFormat](https://godoc.org/go.opencensus.io/trace/propagation#HTTPFormat).
 
@@ -255,5 +257,7 @@ release in which the functionality was marked *Deprecated*.
 [exporter-stackdriver]: https://godoc.org/contrib.go.opencensus.io/exporter/stackdriver
 [exporter-zipkin]: https://godoc.org/go.opencensus.io/exporter/zipkin
 [exporter-jaeger]: https://godoc.org/go.opencensus.io/exporter/jaeger
-[exporter-xray]: https://github.com/census-instrumentation/opencensus-go-exporter-aws
+[exporter-xray]: https://github.com/census-ecosystem/opencensus-go-exporter-aws
 [exporter-datadog]: https://github.com/DataDog/opencensus-go-exporter-datadog
+[exporter-graphite]: https://github.com/census-ecosystem/opencensus-go-exporter-graphite
+[exporter-honeycomb]: https://github.com/honeycombio/opencensus-exporter
diff --git a/appveyor.yml b/appveyor.yml
index 5aa0671..9805788 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -5,8 +5,10 @@ platform: x64
 clone_folder: c:\gopath\src\go.opencensus.io
 
 environment:
-  GOPATH: c:\gopath
-  GOVERSION: 1.8
+  GOPATH: 'c:\gopath'
+  GOVERSION: '1.11'
+  GO111MODULE: 'on'
+  CGO_ENABLED: '0' # See: https://github.com/appveyor/ci/issues/2613
 
 install:
   - set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
@@ -18,7 +20,5 @@ deploy: false
 
 test_script:
   - cd %APPVEYOR_BUILD_FOLDER%
-  - gofmt -w .
-  - go get -v -t .\...
-  - go test -race -v .\...
-  - go vet .\...
+  - go build -v .\...
+  - go test -v .\... # No -race because cgo is disabled
diff --git a/examples/exporter/exporter.go b/examples/exporter/exporter.go
index f909310..43bd73a 100644
--- a/examples/exporter/exporter.go
+++ b/examples/exporter/exporter.go
@@ -15,22 +15,91 @@
 package exporter // import "go.opencensus.io/examples/exporter"
 
 import (
-	"log"
+	"encoding/hex"
+	"fmt"
+	"regexp"
+	"time"
 
 	"go.opencensus.io/stats/view"
 	"go.opencensus.io/trace"
 )
 
+// indent these many spaces
+const indent = "  "
+
+// reZero provides a simple way to detect an empty ID
+var reZero = regexp.MustCompile(`^0+$`)
+
 // PrintExporter is a stats and trace exporter that logs
 // the exported data to the console.
+//
+// The intent is help new users familiarize themselves with the
+// capabilities of opencensus.
+//
+// This should NOT be used for production workloads.
 type PrintExporter struct{}
 
 // ExportView logs the view data.
 func (e *PrintExporter) ExportView(vd *view.Data) {
-	log.Println(vd)
+	for _, row := range vd.Rows {
+		fmt.Printf("%v %-45s", vd.End.Format("15:04:05"), vd.View.Name)
+
+		switch v := row.Data.(type) {
+		case *view.DistributionData:
+			fmt.Printf("distribution: min=%.1f max=%.1f mean=%.1f", v.Min, v.Max, v.Mean)
+		case *view.CountData:
+			fmt.Printf("count:        value=%v", v.Value)
+		case *view.SumData:
+			fmt.Printf("sum:          value=%v", v.Value)
+		case *view.LastValueData:
+			fmt.Printf("last:         value=%v", v.Value)
+		}
+		fmt.Println()
+
+		for _, tag := range row.Tags {
+			fmt.Printf("%v- %v=%v\n", indent, tag.Key.Name(), tag.Value)
+		}
+	}
 }
 
 // ExportSpan logs the trace span.
 func (e *PrintExporter) ExportSpan(vd *trace.SpanData) {
-	log.Println(vd)
+	var (
+		traceID      = hex.EncodeToString(vd.SpanContext.TraceID[:])
+		spanID       = hex.EncodeToString(vd.SpanContext.SpanID[:])
+		parentSpanID = hex.EncodeToString(vd.ParentSpanID[:])
+	)
+	fmt.Println()
+	fmt.Println("#----------------------------------------------")
+	fmt.Println()
+	fmt.Println("TraceID:     ", traceID)
+	fmt.Println("SpanID:      ", spanID)
+	if !reZero.MatchString(parentSpanID) {
+		fmt.Println("ParentSpanID:", parentSpanID)
+	}
+
+	fmt.Println()
+	fmt.Printf("Span:    %v\n", vd.Name)
+	fmt.Printf("Status:  %v [%v]\n", vd.Status.Message, vd.Status.Code)
+	fmt.Printf("Elapsed: %v\n", vd.EndTime.Sub(vd.StartTime).Round(time.Millisecond))
+
+	if len(vd.Annotations) > 0 {
+		fmt.Println()
+		fmt.Println("Annotations:")
+		for _, item := range vd.Annotations {
+			fmt.Print(indent, item.Message)
+			for k, v := range item.Attributes {
+				fmt.Printf(" %v=%v", k, v)
+			}
+			fmt.Println()
+		}
+	}
+
+	if len(vd.Attributes) > 0 {
+		fmt.Println()
+		fmt.Println("Attributes:")
+		for k, v := range vd.Attributes {
+			fmt.Printf("%v- %v=%v\n", indent, k, v)
+		}
+	}
 }
diff --git a/examples/http/helloworld_client/main.go b/examples/http/helloworld_client/main.go
index 3b3186b..0a07536 100644
--- a/examples/http/helloworld_client/main.go
+++ b/examples/http/helloworld_client/main.go
@@ -34,7 +34,9 @@ func main() {
 	view.RegisterExporter(exporter)
 	trace.RegisterExporter(exporter)
 
-	// Always trace for this demo.
+	// Always trace for this demo. In a production application, you should
+	// configure this to a trace.ProbabilitySampler set at the desired
+	// probability.
 	trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
 
 	// Report stats at every second.
diff --git a/examples/http/helloworld_server/main.go b/examples/http/helloworld_server/main.go
index 3f042c1..16ed552 100644
--- a/examples/http/helloworld_server/main.go
+++ b/examples/http/helloworld_server/main.go
@@ -41,7 +41,9 @@ func main() {
 	view.RegisterExporter(exporter)
 	trace.RegisterExporter(exporter)
 
-	// Always trace for this demo.
+	// Always trace for this demo. In a production application, you should
+	// configure this to a trace.ProbabilitySampler set at the desired
+	// probability.
 	trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
 
 	// Report stats at every second.
@@ -52,6 +54,13 @@ func main() {
 	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
 		fmt.Fprintf(w, "hello world")
 
+		// Provide an example of how spans can be annotated with metadata
+		_, span := trace.StartSpan(req.Context(), "child")
+		defer span.End()
+		span.Annotate([]trace.Attribute{trace.StringAttribute("key", "value")}, "something happened")
+		span.AddAttributes(trace.StringAttribute("hello", "world"))
+		time.Sleep(time.Millisecond * 125)
+
 		r, _ := http.NewRequest("GET", "https://example.com", nil)
 
 		// Propagate the trace header info in the outgoing requests.
diff --git a/examples/quickstart/stats.go b/examples/quickstart/stats.go
new file mode 100644
index 0000000..19ddd48
--- /dev/null
+++ b/examples/quickstart/stats.go
@@ -0,0 +1,164 @@
+// 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.
+
+// Command stats implements the stats Quick Start example from:
+//   https://opencensus.io/quickstart/go/metrics/
+package main
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"time"
+
+	"net/http"
+
+	"go.opencensus.io/exporter/prometheus"
+	"go.opencensus.io/stats"
+	"go.opencensus.io/stats/view"
+	"go.opencensus.io/tag"
+	"go.opencensus.io/zpages"
+)
+
+var (
+	// The latency in milliseconds
+	MLatencyMs = stats.Float64("repl/latency", "The latency in milliseconds per REPL loop", "ms")
+
+	// Counts the number of lines read in from standard input
+	MLinesIn = stats.Int64("repl/lines_in", "The number of lines read in", "1")
+
+	// Encounters the number of non EOF(end-of-file) errors.
+	MErrors = stats.Int64("repl/errors", "The number of errors encountered", "1")
+
+	// Counts/groups the lengths of lines read in.
+	MLineLengths = stats.Int64("repl/line_lengths", "The distribution of line lengths", "By")
+)
+
+var (
+	KeyMethod, _ = tag.NewKey("method")
+)
+
+var (
+	LatencyView = &view.View{
+		Name:        "demo/latency",
+		Measure:     MLatencyMs,
+		Description: "The distribution of the latencies",
+
+		// Latency in buckets:
+		// [>=0ms, >=25ms, >=50ms, >=75ms, >=100ms, >=200ms, >=400ms, >=600ms, >=800ms, >=1s, >=2s, >=4s, >=6s]
+		Aggregation: view.Distribution(0, 25, 50, 75, 100, 200, 400, 600, 800, 1000, 2000, 4000, 6000),
+		TagKeys:     []tag.Key{KeyMethod}}
+
+	LineCountView = &view.View{
+		Name:        "demo/lines_in",
+		Measure:     MLinesIn,
+		Description: "The number of lines from standard input",
+		Aggregation: view.Count(),
+	}
+
+	ErrorCountView = &view.View{
+		Name:        "demo/errors",
+		Measure:     MErrors,
+		Description: "The number of errors encountered",
+		Aggregation: view.Count(),
+	}
+
+	LineLengthView = &view.View{
+		Name:        "demo/line_lengths",
+		Description: "Groups the lengths of keys in buckets",
+		Measure:     MLineLengths,
+		// Lengths: [>=0B, >=5B, >=10B, >=15B, >=20B, >=40B, >=60B, >=80, >=100B, >=200B, >=400, >=600, >=800, >=1000]
+		Aggregation: view.Distribution(0, 5, 10, 15, 20, 40, 60, 80, 100, 200, 400, 600, 800, 1000),
+	}
+)
+
+func main() {
+	zpages.Handle(nil, "/debug")
+	go http.ListenAndServe("localhost:8080", nil)
+
+	// Create that Stackdriver stats exporter
+	exporter, err := prometheus.NewExporter(prometheus.Options{})
+	if err != nil {
+		log.Fatalf("Failed to create the Stackdriver stats exporter: %v", err)
+	}
+	http.Handle("/metrics", exporter)
+
+	// Register the stats exporter
+	view.RegisterExporter(exporter)
+
+	// Register the views
+	if err := view.Register(LatencyView, LineCountView, ErrorCountView, LineLengthView); err != nil {
+		log.Fatalf("Failed to register views: %v", err)
+	}
+
+	// But also we can change the metrics reporting period to 2 seconds
+	//view.SetReportingPeriod(2 * time.Second)
+
+	// In a REPL:
+	//   1. Read input
+	//   2. process input
+	br := bufio.NewReader(os.Stdin)
+
+	// repl is the read, evaluate, print, loop
+	for {
+		if err := readEvaluateProcess(br); err != nil {
+			if err == io.EOF {
+				return
+			}
+			log.Fatal(err)
+		}
+	}
+}
+
+// readEvaluateProcess reads a line from the input reader and
+// then processes it. It returns an error if any was encountered.
+func readEvaluateProcess(br *bufio.Reader) error {
+	ctx, err := tag.New(context.Background(), tag.Insert(KeyMethod, "repl"))
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("> ")
+	line, _, err := br.ReadLine()
+	if err != nil {
+		if err != io.EOF {
+			stats.Record(ctx, MErrors.M(1))
+		}
+		return err
+	}
+
+	out, err := processLine(ctx, line)
+	if err != nil {
+		stats.Record(ctx, MErrors.M(1))
+		return err
+	}
+	fmt.Printf("< %s\n\n", out)
+	return nil
+}
+
+// processLine takes in a line of text and
+// transforms it. Currently it just capitalizes it.
+func processLine(ctx context.Context, in []byte) (out []byte, err error) {
+	startTime := time.Now()
+	defer func() {
+		ms := float64(time.Since(startTime).Nanoseconds()) / 1e6
+		stats.Record(ctx, MLinesIn.M(1), MLatencyMs.M(ms), MLineLengths.M(int64(len(in))))
+	}()
+
+	return bytes.ToUpper(in), nil
+}
diff --git a/exemplar/exemplar.go b/exemplar/exemplar.go
new file mode 100644
index 0000000..e676df8
--- /dev/null
+++ b/exemplar/exemplar.go
@@ -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 exemplar implements support for exemplars. Exemplars are additional
+// data associated with each measurement.
+//
+// Their purpose it to provide an example of the kind of thing
+// (request, RPC, trace span, etc.) that resulted in that measurement.
+package exemplar
+
+import (
+	"context"
+	"time"
+)
+
+const (
+	KeyTraceID   = "trace_id"
+	KeySpanID    = "span_id"
+	KeyPrefixTag = "tag:"
+)
+
+// Exemplar is an example data point associated with each bucket of a
+// distribution type aggregation.
+type Exemplar struct {
+	Value       float64     // the value that was recorded
+	Timestamp   time.Time   // the time the value was recorded
+	Attachments Attachments // attachments (if any)
+}
+
+// Attachments is a map of extra values associated with a recorded data point.
+// The map should only be mutated from AttachmentExtractor functions.
+type Attachments map[string]string
+
+// AttachmentExtractor is a function capable of extracting exemplar attachments
+// from the context used to record measurements.
+// The map passed to the function should be mutated and returned. It will
+// initially be nil: the first AttachmentExtractor that would like to add keys to the
+// map is responsible for initializing it.
+type AttachmentExtractor func(ctx context.Context, a Attachments) Attachments
+
+var extractors []AttachmentExtractor
+
+// RegisterAttachmentExtractor registers the given extractor associated with the exemplar
+// type name.
+//
+// Extractors will be used to attempt to extract exemplars from the context
+// associated with each recorded measurement.
+//
+// Packages that support exemplars should register their extractor functions on
+// initialization.
+//
+// RegisterAttachmentExtractor should not be called after any measurements have
+// been recorded.
+func RegisterAttachmentExtractor(e AttachmentExtractor) {
+	extractors = append(extractors, e)
+}
+
+// NewFromContext extracts exemplars from the given context.
+// Each registered AttachmentExtractor (see RegisterAttachmentExtractor) is called in an
+// unspecified order to add attachments to the exemplar.
+func AttachmentsFromContext(ctx context.Context) Attachments {
+	var a Attachments
+	for _, extractor := range extractors {
+		a = extractor(ctx, a)
+	}
+	return a
+}
diff --git a/exporter/jaeger/example/main.go b/exporter/jaeger/example/main.go
index 1caec45..303bc6e 100644
--- a/exporter/jaeger/example/main.go
+++ b/exporter/jaeger/example/main.go
@@ -30,15 +30,19 @@ func main() {
 	// Register the Jaeger exporter to be able to retrieve
 	// the collected spans.
 	exporter, err := jaeger.NewExporter(jaeger.Options{
-		Endpoint:    "http://localhost:14268",
-		ServiceName: "trace-demo",
+		Endpoint: "http://localhost:14268",
+		Process: jaeger.Process{
+			ServiceName: "trace-demo",
+		},
 	})
 	if err != nil {
 		log.Fatal(err)
 	}
 	trace.RegisterExporter(exporter)
 
-	// For demoing purposes, always sample.
+	// For demoing purposes, always sample. In a production application, you should
+	// configure this to a trace.ProbabilitySampler set at the desired
+	// probability.
 	trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
 
 	ctx, span := trace.StartSpan(ctx, "/foo")
diff --git a/exporter/jaeger/jaeger.go b/exporter/jaeger/jaeger.go
index 8adafbb..d108cc0 100644
--- a/exporter/jaeger/jaeger.go
+++ b/exporter/jaeger/jaeger.go
@@ -37,8 +37,14 @@ const defaultServiceName = "OpenCensus"
 type Options struct {
 	// Endpoint is the Jaeger HTTP Thrift endpoint.
 	// For example, http://localhost:14268.
+	//
+	// Deprecated: Use CollectorEndpoint instead.
 	Endpoint string
 
+	// CollectorEndpoint is the full url to the Jaeger HTTP Thrift collector.
+	// For example, http://localhost:14268/api/traces
+	CollectorEndpoint string
+
 	// AgentEndpoint instructs exporter to send spans to jaeger-agent at this address.
 	// For example, localhost:6831.
 	AgentEndpoint string
@@ -58,21 +64,28 @@ type Options struct {
 	Password string
 
 	// ServiceName is the Jaeger service name.
+	// Deprecated: Specify Process instead.
 	ServiceName string
+
+	// Process contains the information about the exporting process.
+	Process Process
 }
 
 // NewExporter returns a trace.Exporter implementation that exports
 // the collected spans to Jaeger.
 func NewExporter(o Options) (*Exporter, error) {
-	endpoint := o.Endpoint
-	if endpoint == "" && o.AgentEndpoint == "" {
+	if o.Endpoint == "" && o.CollectorEndpoint == "" && o.AgentEndpoint == "" {
 		return nil, errors.New("missing endpoint for Jaeger exporter")
 	}
 
+	var endpoint string
 	var client *agentClientUDP
 	var err error
-	if endpoint != "" {
-		endpoint = endpoint + "/api/traces?format=jaeger.thrift"
+	if o.Endpoint != "" {
+		endpoint = o.Endpoint + "/api/traces?format=jaeger.thrift"
+		log.Printf("Endpoint has been deprecated. Please use CollectorEndpoint instead.")
+	} else if o.CollectorEndpoint != "" {
+		endpoint = o.CollectorEndpoint
 	} else {
 		client, err = newAgentClientUDP(o.AgentEndpoint, udpPacketMaxLength)
 		if err != nil {
@@ -86,17 +99,27 @@ func NewExporter(o Options) (*Exporter, error) {
 		}
 		log.Printf("Error when uploading spans to Jaeger: %v", err)
 	}
-	service := o.ServiceName
-	if service == "" {
+	service := o.Process.ServiceName
+	if service == "" && o.ServiceName != "" {
+		// fallback to old service name if specified
+		service = o.ServiceName
+	} else if service == "" {
 		service = defaultServiceName
 	}
+	tags := make([]*gen.Tag, len(o.Process.Tags))
+	for i, tag := range o.Process.Tags {
+		tags[i] = attributeToTag(tag.key, tag.value)
+	}
 	e := &Exporter{
 		endpoint:      endpoint,
 		agentEndpoint: o.AgentEndpoint,
 		client:        client,
 		username:      o.Username,
 		password:      o.Password,
-		service:       service,
+		process: &gen.Process{
+			ServiceName: service,
+			Tags:        tags,
+		},
 	}
 	bundler := bundler.NewBundler((*gen.Span)(nil), func(bundle interface{}) {
 		if err := e.upload(bundle.([]*gen.Span)); err != nil {
@@ -107,11 +130,43 @@ func NewExporter(o Options) (*Exporter, error) {
 	return e, nil
 }
 
+// Process contains the information exported to jaeger about the source
+// of the trace data.
+type Process struct {
+	// ServiceName is the Jaeger service name.
+	ServiceName string
+
+	// Tags are added to Jaeger Process exports
+	Tags []Tag
+}
+
+// Tag defines a key-value pair
+// It is limited to the possible conversions to *jaeger.Tag by attributeToTag
+type Tag struct {
+	key   string
+	value interface{}
+}
+
+// BoolTag creates a new tag of type bool, exported as jaeger.TagType_BOOL
+func BoolTag(key string, value bool) Tag {
+	return Tag{key, value}
+}
+
+// StringTag creates a new tag of type string, exported as jaeger.TagType_STRING
+func StringTag(key string, value string) Tag {
+	return Tag{key, value}
+}
+
+// Int64Tag creates a new tag of type int64, exported as jaeger.TagType_LONG
+func Int64Tag(key string, value int64) Tag {
+	return Tag{key, value}
+}
+
 // Exporter is an implementation of trace.Exporter that uploads spans to Jaeger.
 type Exporter struct {
 	endpoint      string
 	agentEndpoint string
-	service       string
+	process       *gen.Process
 	bundler       *bundler.Bundler
 	client        *agentClientUDP
 
@@ -230,10 +285,8 @@ func (e *Exporter) Flush() {
 
 func (e *Exporter) upload(spans []*gen.Span) error {
 	batch := &gen.Batch{
-		Spans: spans,
-		Process: &gen.Process{
-			ServiceName: e.service,
-		},
+		Spans:   spans,
+		Process: e.process,
 	}
 	if e.endpoint != "" {
 		return e.uploadCollector(batch)
diff --git a/exporter/zipkin/example/main.go b/exporter/zipkin/example/main.go
index 24ec878..9466c98 100644
--- a/exporter/zipkin/example/main.go
+++ b/exporter/zipkin/example/main.go
@@ -41,7 +41,9 @@ func main() {
 	exporter := zipkin.NewExporter(reporter, localEndpoint)
 	trace.RegisterExporter(exporter)
 
-	// For example purposes, sample every trace.
+	// For example purposes, sample every trace. In a production application, you should
+	// configure this to a trace.ProbabilitySampler set at the desired
+	// probability.
 	trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
 
 	ctx := context.Background()
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1236f4c
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,25 @@
+module go.opencensus.io
+
+require (
+	git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999
+	github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973
+	github.com/ghodss/yaml v1.0.0 // indirect
+	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
+	github.com/golang/protobuf v1.2.0
+	github.com/google/go-cmp v0.2.0
+	github.com/grpc-ecosystem/grpc-gateway v1.5.0 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.1
+	github.com/openzipkin/zipkin-go v0.1.1
+	github.com/prometheus/client_golang v0.8.0
+	github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910
+	github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e
+	github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273
+	golang.org/x/net v0.0.0-20180906233101-161cd47e91fd
+	golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
+	golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e
+	golang.org/x/text v0.3.0
+	google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf
+	google.golang.org/genproto v0.0.0-20180831171423-11092d34479b
+	google.golang.org/grpc v1.14.0
+	gopkg.in/yaml.v2 v2.2.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..3e0bab8
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,48 @@
+git.apache.org/thrift.git v0.0.0-20180807212849-6e67faa92827/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999 h1:sihTnRgTOUSCQz0iS0pjZuFQy/z7GXCJgSBg3+rZKHw=
+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/grpc-ecosystem/grpc-gateway v1.5.0 h1:WcmKMm43DR7RdtlkEXQJyo5ws8iTp98CyhCCbOHMvNI=
+github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOIQAeIw=
+github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/prometheus/client_golang v0.8.0 h1:1921Yw9Gc3iSc4VQh3PIoOqgPCZS7G/4xQNVUp8Mda8=
+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e h1:n/3MEhJQjQxrOUCzh1Y3Re6aJUUWRp2M9+Oc3eVn/54=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 h1:agujYaXJSxSo18YNX3jzl+4G6Bstwt+kqv47GS12uL0=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+golang.org/x/net v0.0.0-20180821023952-922f4815f713/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+google.golang.org/api v0.0.0-20180818000503-e21acd801f91/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf h1:rjxqQmxjyqerRKEj+tZW+MCm4LgpFXu18bsEoCMgDsk=
+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.14.0 h1:ArxJuB1NWfPY6r9Gp9gqwplT0Ge7nqv9msgu03lHLmo=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/opencensus.go b/opencensus.go
index 3b4e0c6..7faf9e8 100644
--- a/opencensus.go
+++ b/opencensus.go
@@ -17,5 +17,5 @@ package opencensus // import "go.opencensus.io"
 
 // Version is the current release version of OpenCensus in use.
 func Version() string {
-	return "0.16.0"
+	return "0.19.0"
 }
diff --git a/plugin/ocgrpc/stats_common.go b/plugin/ocgrpc/stats_common.go
index 119bbda..1737809 100644
--- a/plugin/ocgrpc/stats_common.go
+++ b/plugin/ocgrpc/stats_common.go
@@ -142,18 +142,21 @@ func handleRPCEnd(ctx context.Context, s *stats.End) {
 
 	latencyMillis := float64(elapsedTime) / float64(time.Millisecond)
 	if s.Client {
-		ctx, _ = tag.New(ctx,
-			tag.Upsert(KeyClientMethod, methodName(d.method)),
-			tag.Upsert(KeyClientStatus, st))
-		ocstats.Record(ctx,
+		ocstats.RecordWithTags(ctx,
+			[]tag.Mutator{
+				tag.Upsert(KeyClientMethod, methodName(d.method)),
+				tag.Upsert(KeyClientStatus, st),
+			},
 			ClientSentBytesPerRPC.M(atomic.LoadInt64(&d.sentBytes)),
 			ClientSentMessagesPerRPC.M(atomic.LoadInt64(&d.sentCount)),
 			ClientReceivedMessagesPerRPC.M(atomic.LoadInt64(&d.recvCount)),
 			ClientReceivedBytesPerRPC.M(atomic.LoadInt64(&d.recvBytes)),
 			ClientRoundtripLatency.M(latencyMillis))
 	} else {
-		ctx, _ = tag.New(ctx, tag.Upsert(KeyServerStatus, st))
-		ocstats.Record(ctx,
+		ocstats.RecordWithTags(ctx,
+			[]tag.Mutator{
+				tag.Upsert(KeyServerStatus, st),
+			},
 			ServerSentBytesPerRPC.M(atomic.LoadInt64(&d.sentBytes)),
 			ServerSentMessagesPerRPC.M(atomic.LoadInt64(&d.sentCount)),
 			ServerReceivedMessagesPerRPC.M(atomic.LoadInt64(&d.recvCount)),
diff --git a/plugin/ochttp/client.go b/plugin/ochttp/client.go
index 68faf24..da815b2 100644
--- a/plugin/ochttp/client.go
+++ b/plugin/ochttp/client.go
@@ -47,6 +47,10 @@ type Transport struct {
 	// for spans started by this transport.
 	StartOptions trace.StartOptions
 
+	// GetStartOptions allows to set start options per request. If set,
+	// StartOptions is going to be ignored.
+	GetStartOptions func(*http.Request) trace.StartOptions
+
 	// NameFromRequest holds the function to use for generating the span name
 	// from the information found in the outgoing HTTP Request. By default the
 	// name equals the URL Path.
@@ -75,11 +79,17 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
 	if spanNameFormatter == nil {
 		spanNameFormatter = spanNameFromURL
 	}
+
+	startOpts := t.StartOptions
+	if t.GetStartOptions != nil {
+		startOpts = t.GetStartOptions(req)
+	}
+
 	rt = &traceTransport{
 		base:   rt,
 		format: format,
 		startOptions: trace.StartOptions{
-			Sampler:  t.StartOptions.Sampler,
+			Sampler:  startOpts.Sampler,
 			SpanKind: trace.SpanKindClient,
 		},
 		formatSpanName: spanNameFormatter,
diff --git a/plugin/ochttp/client_stats.go b/plugin/ochttp/client_stats.go
index 9b286b9..066ebb8 100644
--- a/plugin/ochttp/client_stats.go
+++ b/plugin/ochttp/client_stats.go
@@ -34,8 +34,11 @@ type statsTransport struct {
 // RoundTrip implements http.RoundTripper, delegating to Base and recording stats for the request.
 func (t statsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 	ctx, _ := tag.New(req.Context(),
+		tag.Upsert(KeyClientHost, req.URL.Host),
 		tag.Upsert(Host, req.URL.Host),
+		tag.Upsert(KeyClientPath, req.URL.Path),
 		tag.Upsert(Path, req.URL.Path),
+		tag.Upsert(KeyClientMethod, req.Method),
 		tag.Upsert(Method, req.Method))
 	req = req.WithContext(ctx)
 	track := &tracker{
@@ -92,15 +95,22 @@ var _ io.ReadCloser = (*tracker)(nil)
 
 func (t *tracker) end() {
 	t.endOnce.Do(func() {
+		latencyMs := float64(time.Since(t.start)) / float64(time.Millisecond)
 		m := []stats.Measurement{
-			ClientLatency.M(float64(time.Since(t.start)) / float64(time.Millisecond)),
+			ClientSentBytes.M(t.reqSize),
+			ClientReceivedBytes.M(t.respSize),
+			ClientRoundtripLatency.M(latencyMs),
+			ClientLatency.M(latencyMs),
 			ClientResponseBytes.M(t.respSize),
 		}
 		if t.reqSize >= 0 {
 			m = append(m, ClientRequestBytes.M(t.reqSize))
 		}
-		ctx, _ := tag.New(t.ctx, tag.Upsert(StatusCode, strconv.Itoa(t.statusCode)))
-		stats.Record(ctx, m...)
+
+		stats.RecordWithTags(t.ctx, []tag.Mutator{
+			tag.Upsert(StatusCode, strconv.Itoa(t.statusCode)),
+			tag.Upsert(KeyClientStatus, strconv.Itoa(t.statusCode)),
+		}, m...)
 	})
 }
 
diff --git a/plugin/ochttp/propagation/tracecontext/propagation.go b/plugin/ochttp/propagation/tracecontext/propagation.go
index e4f44ee..65ab1e9 100644
--- a/plugin/ochttp/propagation/tracecontext/propagation.go
+++ b/plugin/ochttp/propagation/tracecontext/propagation.go
@@ -20,18 +20,26 @@ import (
 	"encoding/hex"
 	"fmt"
 	"net/http"
+	"net/textproto"
+	"regexp"
 	"strings"
 
 	"go.opencensus.io/trace"
 	"go.opencensus.io/trace/propagation"
+	"go.opencensus.io/trace/tracestate"
 )
 
 const (
-	supportedVersion = 0
-	maxVersion       = 254
-	header           = "traceparent"
+	supportedVersion  = 0
+	maxVersion        = 254
+	maxTracestateLen  = 512
+	traceparentHeader = "traceparent"
+	tracestateHeader  = "tracestate"
+	trimOWSRegexFmt   = `^[\x09\x20]*(.*[^\x20\x09])[\x09\x20]*$`
 )
 
+var trimOWSRegExp = regexp.MustCompile(trimOWSRegexFmt)
+
 var _ propagation.HTTPFormat = (*HTTPFormat)(nil)
 
 // HTTPFormat implements the TraceContext trace propagation format.
@@ -39,63 +47,141 @@ type HTTPFormat struct{}
 
 // SpanContextFromRequest extracts a span context from incoming requests.
 func (f *HTTPFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) {
-	h := req.Header.Get(header)
-	if h == "" {
+	h, ok := getRequestHeader(req, traceparentHeader, false)
+	if !ok {
 		return trace.SpanContext{}, false
 	}
 	sections := strings.Split(h, "-")
-	if len(sections) < 3 {
+	if len(sections) < 4 {
 		return trace.SpanContext{}, false
 	}
 
+	if len(sections[0]) != 2 {
+		return trace.SpanContext{}, false
+	}
 	ver, err := hex.DecodeString(sections[0])
 	if err != nil {
 		return trace.SpanContext{}, false
 	}
-	if len(ver) == 0 || int(ver[0]) > supportedVersion || int(ver[0]) > maxVersion {
+	version := int(ver[0])
+	if version > maxVersion {
 		return trace.SpanContext{}, false
 	}
 
-	tid, err := hex.DecodeString(sections[1])
-	if err != nil {
+	if version == 0 && len(sections) != 4 {
 		return trace.SpanContext{}, false
 	}
-	if len(tid) != 16 {
+
+	if len(sections[1]) != 32 {
+		return trace.SpanContext{}, false
+	}
+	tid, err := hex.DecodeString(sections[1])
+	if err != nil {
 		return trace.SpanContext{}, false
 	}
 	copy(sc.TraceID[:], tid)
 
-	sid, err := hex.DecodeString(sections[2])
-	if err != nil {
+	if len(sections[2]) != 16 {
 		return trace.SpanContext{}, false
 	}
-	if len(sid) != 8 {
+	sid, err := hex.DecodeString(sections[2])
+	if err != nil {
 		return trace.SpanContext{}, false
 	}
 	copy(sc.SpanID[:], sid)
 
-	if len(sections) == 4 {
-		opts, err := hex.DecodeString(sections[3])
-		if err != nil || len(opts) < 1 {
-			return trace.SpanContext{}, false
-		}
-		sc.TraceOptions = trace.TraceOptions(opts[0])
+	opts, err := hex.DecodeString(sections[3])
+	if err != nil || len(opts) < 1 {
+		return trace.SpanContext{}, false
 	}
+	sc.TraceOptions = trace.TraceOptions(opts[0])
 
 	// Don't allow all zero trace or span ID.
 	if sc.TraceID == [16]byte{} || sc.SpanID == [8]byte{} {
 		return trace.SpanContext{}, false
 	}
 
+	sc.Tracestate = tracestateFromRequest(req)
 	return sc, true
 }
 
-// SpanContextToRequest modifies the given request to include a header.
+// getRequestHeader returns a combined header field according to RFC7230 section 3.2.2.
+// If commaSeparated is true, multiple header fields with the same field name using be
+// combined using ",".
+// If no header was found using the given name, "ok" would be false.
+// If more than one headers was found using the given name, while commaSeparated is false,
+// "ok" would be false.
+func getRequestHeader(req *http.Request, name string, commaSeparated bool) (hdr string, ok bool) {
+	v := req.Header[textproto.CanonicalMIMEHeaderKey(name)]
+	switch len(v) {
+	case 0:
+		return "", false
+	case 1:
+		return v[0], true
+	default:
+		return strings.Join(v, ","), commaSeparated
+	}
+}
+
+// TODO(rghetia): return an empty Tracestate when parsing tracestate header encounters an error.
+// Revisit to return additional boolean value to indicate parsing error when following issues
+// are resolved.
+// https://github.com/w3c/distributed-tracing/issues/172
+// https://github.com/w3c/distributed-tracing/issues/175
+func tracestateFromRequest(req *http.Request) *tracestate.Tracestate {
+	h, _ := getRequestHeader(req, tracestateHeader, true)
+	if h == "" {
+		return nil
+	}
+
+	var entries []tracestate.Entry
+	pairs := strings.Split(h, ",")
+	hdrLenWithoutOWS := len(pairs) - 1 // Number of commas
+	for _, pair := range pairs {
+		matches := trimOWSRegExp.FindStringSubmatch(pair)
+		if matches == nil {
+			return nil
+		}
+		pair = matches[1]
+		hdrLenWithoutOWS += len(pair)
+		if hdrLenWithoutOWS > maxTracestateLen {
+			return nil
+		}
+		kv := strings.Split(pair, "=")
+		if len(kv) != 2 {
+			return nil
+		}
+		entries = append(entries, tracestate.Entry{Key: kv[0], Value: kv[1]})
+	}
+	ts, err := tracestate.New(nil, entries...)
+	if err != nil {
+		return nil
+	}
+
+	return ts
+}
+
+func tracestateToRequest(sc trace.SpanContext, req *http.Request) {
+	var pairs = make([]string, 0, len(sc.Tracestate.Entries()))
+	if sc.Tracestate != nil {
+		for _, entry := range sc.Tracestate.Entries() {
+			pairs = append(pairs, strings.Join([]string{entry.Key, entry.Value}, "="))
+		}
+		h := strings.Join(pairs, ",")
+
+		if h != "" && len(h) <= maxTracestateLen {
+			req.Header.Set(tracestateHeader, h)
+		}
+	}
+}
+
+// SpanContextToRequest modifies the given request to include traceparent and tracestate headers.
 func (f *HTTPFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) {
 	h := fmt.Sprintf("%x-%x-%x-%x",
 		[]byte{supportedVersion},
 		sc.TraceID[:],
 		sc.SpanID[:],
 		[]byte{byte(sc.TraceOptions)})
-	req.Header.Set(header, h)
+	req.Header.Set(traceparentHeader, h)
+	tracestateToRequest(sc, req)
 }
diff --git a/plugin/ochttp/route.go b/plugin/ochttp/route.go
new file mode 100644
index 0000000..dbe22d5
--- /dev/null
+++ b/plugin/ochttp/route.go
@@ -0,0 +1,51 @@
+// 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 ochttp
+
+import (
+	"net/http"
+
+	"go.opencensus.io/tag"
+)
+
+// WithRouteTag returns an http.Handler that records stats with the
+// http_server_route tag set to the given value.
+func WithRouteTag(handler http.Handler, route string) http.Handler {
+	return taggedHandlerFunc(func(w http.ResponseWriter, r *http.Request) []tag.Mutator {
+		addRoute := []tag.Mutator{tag.Upsert(KeyServerRoute, route)}
+		ctx, _ := tag.New(r.Context(), addRoute...)
+		r = r.WithContext(ctx)
+		handler.ServeHTTP(w, r)
+		return addRoute
+	})
+}
+
+// taggedHandlerFunc is a http.Handler that returns tags describing the
+// processing of the request. These tags will be recorded along with the
+// measures in this package at the end of the request.
+type taggedHandlerFunc func(w http.ResponseWriter, r *http.Request) []tag.Mutator
+
+func (h taggedHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	tags := h(w, r)
+	if a, ok := r.Context().Value(addedTagsKey{}).(*addedTags); ok {
+		a.t = append(a.t, tags...)
+	}
+}
+
+type addedTagsKey struct{}
+
+type addedTags struct {
+	t []tag.Mutator
+}
diff --git a/plugin/ochttp/server.go b/plugin/ochttp/server.go
index 72aa8c2..ff72de9 100644
--- a/plugin/ochttp/server.go
+++ b/plugin/ochttp/server.go
@@ -56,6 +56,10 @@ type Handler struct {
 	// for spans started by this transport.
 	StartOptions trace.StartOptions
 
+	// GetStartOptions allows to set start options per request. If set,
+	// StartOptions is going to be ignored.
+	GetStartOptions func(*http.Request) trace.StartOptions
+
 	// IsPublicEndpoint should be set to true for publicly accessible HTTP(S)
 	// servers. If true, any trace metadata set on the incoming request will
 	// be added as a linked trace instead of being added as a parent of the
@@ -69,15 +73,16 @@ type Handler struct {
 }
 
 func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	var traceEnd, statsEnd func()
-	r, traceEnd = h.startTrace(w, r)
+	var tags addedTags
+	r, traceEnd := h.startTrace(w, r)
 	defer traceEnd()
-	w, statsEnd = h.startStats(w, r)
-	defer statsEnd()
+	w, statsEnd := h.startStats(w, r)
+	defer statsEnd(&tags)
 	handler := h.Handler
 	if handler == nil {
 		handler = http.DefaultServeMux
 	}
+	r = r.WithContext(context.WithValue(r.Context(), addedTagsKey{}, &tags))
 	handler.ServeHTTP(w, r)
 }
 
@@ -92,15 +97,21 @@ func (h *Handler) startTrace(w http.ResponseWriter, r *http.Request) (*http.Requ
 		name = h.FormatSpanName(r)
 	}
 	ctx := r.Context()
+
+	startOpts := h.StartOptions
+	if h.GetStartOptions != nil {
+		startOpts = h.GetStartOptions(r)
+	}
+
 	var span *trace.Span
 	sc, ok := h.extractSpanContext(r)
 	if ok && !h.IsPublicEndpoint {
 		ctx, span = trace.StartSpanWithRemoteParent(ctx, name, sc,
-			trace.WithSampler(h.StartOptions.Sampler),
+			trace.WithSampler(startOpts.Sampler),
 			trace.WithSpanKind(trace.SpanKindServer))
 	} else {
 		ctx, span = trace.StartSpan(ctx, name,
-			trace.WithSampler(h.StartOptions.Sampler),
+			trace.WithSampler(startOpts.Sampler),
 			trace.WithSpanKind(trace.SpanKindServer),
 		)
 		if ok {
@@ -123,7 +134,7 @@ func (h *Handler) extractSpanContext(r *http.Request) (trace.SpanContext, bool)
 	return h.Propagation.SpanContextFromRequest(r)
 }
 
-func (h *Handler) startStats(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, func()) {
+func (h *Handler) startStats(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, func(tags *addedTags)) {
 	ctx, _ := tag.New(r.Context(),
 		tag.Upsert(Host, r.URL.Host),
 		tag.Upsert(Path, r.URL.Path),
@@ -157,7 +168,9 @@ type trackingResponseWriter struct {
 // Compile time assertion for ResponseWriter interface
 var _ http.ResponseWriter = (*trackingResponseWriter)(nil)
 
-func (t *trackingResponseWriter) end() {
+var logTagsErrorOnce sync.Once
+
+func (t *trackingResponseWriter) end(tags *addedTags) {
 	t.endOnce.Do(func() {
 		if t.statusCode == 0 {
 			t.statusCode = 200
@@ -165,6 +178,7 @@ func (t *trackingResponseWriter) end() {
 
 		span := trace.FromContext(t.ctx)
 		span.SetStatus(TraceStatus(t.statusCode, t.statusLine))
+		span.AddAttributes(trace.Int64Attribute(StatusCodeAttribute, int64(t.statusCode)))
 
 		m := []stats.Measurement{
 			ServerLatency.M(float64(time.Since(t.start)) / float64(time.Millisecond)),
@@ -173,8 +187,10 @@ func (t *trackingResponseWriter) end() {
 		if t.reqSize >= 0 {
 			m = append(m, ServerRequestBytes.M(t.reqSize))
 		}
-		ctx, _ := tag.New(t.ctx, tag.Upsert(StatusCode, strconv.Itoa(t.statusCode)))
-		stats.Record(ctx, m...)
+		allTags := make([]tag.Mutator, len(tags.t)+1)
+		allTags[0] = tag.Upsert(StatusCode, strconv.Itoa(t.statusCode))
+		copy(allTags[1:], tags.t)
+		stats.RecordWithTags(t.ctx, allTags, m...)
 	})
 }
 
diff --git a/plugin/ochttp/span_annotator.go b/plugin/ochttp/span_annotating_client_trace.go
similarity index 90%
rename from plugin/ochttp/span_annotator.go
rename to plugin/ochttp/span_annotating_client_trace.go
index 1282287..05c6c56 100644
--- a/plugin/ochttp/span_annotator.go
+++ b/plugin/ochttp/span_annotating_client_trace.go
@@ -27,9 +27,18 @@ type spanAnnotator struct {
 	sp *trace.Span
 }
 
-// NewSpanAnnotator returns a httptrace.ClientTrace which annotates all emitted
-// httptrace events on the provided Span.
-func NewSpanAnnotator(_ *http.Request, s *trace.Span) *httptrace.ClientTrace {
+// TODO: Remove NewSpanAnnotator at the next release.
+
+// NewSpanAnnotator returns a httptrace.ClientTrace which annotates
+// all emitted httptrace events on the provided Span.
+// Deprecated: Use NewSpanAnnotatingClientTrace instead
+func NewSpanAnnotator(r *http.Request, s *trace.Span) *httptrace.ClientTrace {
+	return NewSpanAnnotatingClientTrace(r, s)
+}
+
+// NewSpanAnnotatingClientTrace returns a httptrace.ClientTrace which annotates
+// all emitted httptrace events on the provided Span.
+func NewSpanAnnotatingClientTrace(_ *http.Request, s *trace.Span) *httptrace.ClientTrace {
 	sa := spanAnnotator{sp: s}
 
 	return &httptrace.ClientTrace{
diff --git a/plugin/ochttp/stats.go b/plugin/ochttp/stats.go
index 19a8825..46dcc8e 100644
--- a/plugin/ochttp/stats.go
+++ b/plugin/ochttp/stats.go
@@ -22,10 +22,33 @@ import (
 
 // The following client HTTP measures are supported for use in custom views.
 var (
-	ClientRequestCount  = stats.Int64("opencensus.io/http/client/request_count", "Number of HTTP requests started", stats.UnitDimensionless)
-	ClientRequestBytes  = stats.Int64("opencensus.io/http/client/request_bytes", "HTTP request body size if set as ContentLength (uncompressed)", stats.UnitBytes)
+	// Deprecated: Use a Count aggregation over one of the other client measures to achieve the same effect.
+	ClientRequestCount = stats.Int64("opencensus.io/http/client/request_count", "Number of HTTP requests started", stats.UnitDimensionless)
+	// Deprecated: Use ClientSentBytes.
+	ClientRequestBytes = stats.Int64("opencensus.io/http/client/request_bytes", "HTTP request body size if set as ContentLength (uncompressed)", stats.UnitBytes)
+	// Deprecated: Use ClientReceivedBytes.
 	ClientResponseBytes = stats.Int64("opencensus.io/http/client/response_bytes", "HTTP response body size (uncompressed)", stats.UnitBytes)
-	ClientLatency       = stats.Float64("opencensus.io/http/client/latency", "End-to-end latency", stats.UnitMilliseconds)
+	// Deprecated: Use ClientRoundtripLatency.
+	ClientLatency = stats.Float64("opencensus.io/http/client/latency", "End-to-end latency", stats.UnitMilliseconds)
+)
+
+// Client measures supported for use in custom views.
+var (
+	ClientSentBytes = stats.Int64(
+		"opencensus.io/http/client/sent_bytes",
+		"Total bytes sent in request body (not including headers)",
+		stats.UnitBytes,
+	)
+	ClientReceivedBytes = stats.Int64(
+		"opencensus.io/http/client/received_bytes",
+		"Total bytes received in response bodies (not including headers but including error responses with bodies)",
+		stats.UnitBytes,
+	)
+	ClientRoundtripLatency = stats.Float64(
+		"opencensus.io/http/client/roundtrip_latency",
+		"Time between first byte of request headers sent to last byte of response received, or terminal error",
+		stats.UnitMilliseconds,
+	)
 )
 
 // The following server HTTP measures are supported for use in custom views:
@@ -60,6 +83,23 @@ var (
 
 	// Method is the HTTP method of the request, capitalized (GET, POST, etc.).
 	Method, _ = tag.NewKey("http.method")
+
+	// KeyServerRoute is a low cardinality string representing the logical
+	// handler of the request. This is usually the pattern registered on the a
+	// ServeMux (or similar string).
+	KeyServerRoute, _ = tag.NewKey("http_server_route")
+)
+
+// Client tag keys.
+var (
+	// KeyClientMethod is the HTTP method, capitalized (i.e. GET, POST, PUT, DELETE, etc.).
+	KeyClientMethod, _ = tag.NewKey("http_client_method")
+	// KeyClientPath is the URL path (not including query string).
+	KeyClientPath, _ = tag.NewKey("http_client_path")
+	// KeyClientStatus is the HTTP status code as an integer (e.g. 200, 404, 500.), or "error" if no response status line was received.
+	KeyClientStatus, _ = tag.NewKey("http_client_status")
+	// KeyClientHost is the value of the request Host header.
+	KeyClientHost, _ = tag.NewKey("http_client_host")
 )
 
 // Default distributions used by views in this package.
@@ -69,8 +109,43 @@ var (
 )
 
 // Package ochttp provides some convenience views.
-// You need to register the views for data to actually be collected.
+// You still need to register these views for data to actually be collected.
+var (
+	ClientSentBytesDistribution = &view.View{
+		Name:        "opencensus.io/http/client/sent_bytes",
+		Measure:     ClientSentBytes,
+		Aggregation: DefaultSizeDistribution,
+		Description: "Total bytes sent in request body (not including headers), by HTTP method and response status",
+		TagKeys:     []tag.Key{KeyClientMethod, KeyClientStatus},
+	}
+
+	ClientReceivedBytesDistribution = &view.View{
+		Name:        "opencensus.io/http/client/received_bytes",
+		Measure:     ClientReceivedBytes,
+		Aggregation: DefaultSizeDistribution,
+		Description: "Total bytes received in response bodies (not including headers but including error responses with bodies), by HTTP method and response status",
+		TagKeys:     []tag.Key{KeyClientMethod, KeyClientStatus},
+	}
+
+	ClientRoundtripLatencyDistribution = &view.View{
+		Name:        "opencensus.io/http/client/roundtrip_latency",
+		Measure:     ClientRoundtripLatency,
+		Aggregation: DefaultLatencyDistribution,
+		Description: "End-to-end latency, by HTTP method and response status",
+		TagKeys:     []tag.Key{KeyClientMethod, KeyClientStatus},
+	}
+
+	ClientCompletedCount = &view.View{
+		Name:        "opencensus.io/http/client/completed_count",
+		Measure:     ClientRoundtripLatency,
+		Aggregation: view.Count(),
+		Description: "Count of completed requests, by HTTP method and response status",
+		TagKeys:     []tag.Key{KeyClientMethod, KeyClientStatus},
+	}
+)
+
 var (
+	// Deprecated: No direct replacement, but see ClientCompletedCount.
 	ClientRequestCountView = &view.View{
 		Name:        "opencensus.io/http/client/request_count",
 		Description: "Count of HTTP requests started",
@@ -78,43 +153,50 @@ var (
 		Aggregation: view.Count(),
 	}
 
+	// Deprecated: Use ClientSentBytesDistribution.
 	ClientRequestBytesView = &view.View{
 		Name:        "opencensus.io/http/client/request_bytes",
 		Description: "Size distribution of HTTP request body",
-		Measure:     ClientRequestBytes,
+		Measure:     ClientSentBytes,
 		Aggregation: DefaultSizeDistribution,
 	}
 
+	// Deprecated: Use ClientReceivedBytesDistribution.
 	ClientResponseBytesView = &view.View{
 		Name:        "opencensus.io/http/client/response_bytes",
 		Description: "Size distribution of HTTP response body",
-		Measure:     ClientResponseBytes,
+		Measure:     ClientReceivedBytes,
 		Aggregation: DefaultSizeDistribution,
 	}
 
+	// Deprecated: Use ClientRoundtripLatencyDistribution.
 	ClientLatencyView = &view.View{
 		Name:        "opencensus.io/http/client/latency",
 		Description: "Latency distribution of HTTP requests",
-		Measure:     ClientLatency,
+		Measure:     ClientRoundtripLatency,
 		Aggregation: DefaultLatencyDistribution,
 	}
 
+	// Deprecated: Use ClientCompletedCount.
 	ClientRequestCountByMethod = &view.View{
 		Name:        "opencensus.io/http/client/request_count_by_method",
 		Description: "Client request count by HTTP method",
 		TagKeys:     []tag.Key{Method},
-		Measure:     ClientRequestCount,
+		Measure:     ClientSentBytes,
 		Aggregation: view.Count(),
 	}
 
+	// Deprecated: Use ClientCompletedCount.
 	ClientResponseCountByStatusCode = &view.View{
 		Name:        "opencensus.io/http/client/response_count_by_status_code",
 		Description: "Client response count by status code",
 		TagKeys:     []tag.Key{StatusCode},
-		Measure:     ClientLatency,
+		Measure:     ClientRoundtripLatency,
 		Aggregation: view.Count(),
 	}
+)
 
+var (
 	ServerRequestCountView = &view.View{
 		Name:        "opencensus.io/http/server/request_count",
 		Description: "Count of HTTP requests started",
@@ -161,6 +243,7 @@ var (
 )
 
 // DefaultClientViews are the default client views provided by this package.
+// Deprecated: No replacement. Register the views you would like individually.
 var DefaultClientViews = []*view.View{
 	ClientRequestCountView,
 	ClientRequestBytesView,
@@ -171,6 +254,7 @@ var DefaultClientViews = []*view.View{
 }
 
 // DefaultServerViews are the default server views provided by this package.
+// Deprecated: No replacement. Register the views you would like individually.
 var DefaultServerViews = []*view.View{
 	ServerRequestCountView,
 	ServerRequestBytesView,
diff --git a/plugin/ochttp/trace.go b/plugin/ochttp/trace.go
index 980b639..819a2d5 100644
--- a/plugin/ochttp/trace.go
+++ b/plugin/ochttp/trace.go
@@ -66,6 +66,16 @@ func (t *traceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 	}
 
 	if t.format != nil {
+		// SpanContextToRequest will modify its Request argument, which is
+		// contrary to the contract for http.RoundTripper, so we need to
+		// pass it a copy of the Request.
+		// However, the Request struct itself was already copied by
+		// the WithContext calls above and so we just need to copy the header.
+		header := make(http.Header)
+		for k, v := range req.Header {
+			header[k] = v
+		}
+		req.Header = header
 		t.format.SpanContextToRequest(span.SpanContext(), req)
 	}
 
@@ -186,23 +196,23 @@ func TraceStatus(httpStatusCode int, statusLine string) trace.Status {
 }
 
 var codeToStr = map[int32]string{
-	trace.StatusCodeOK:                 `"OK"`,
-	trace.StatusCodeCancelled:          `"CANCELLED"`,
-	trace.StatusCodeUnknown:            `"UNKNOWN"`,
-	trace.StatusCodeInvalidArgument:    `"INVALID_ARGUMENT"`,
-	trace.StatusCodeDeadlineExceeded:   `"DEADLINE_EXCEEDED"`,
-	trace.StatusCodeNotFound:           `"NOT_FOUND"`,
-	trace.StatusCodeAlreadyExists:      `"ALREADY_EXISTS"`,
-	trace.StatusCodePermissionDenied:   `"PERMISSION_DENIED"`,
-	trace.StatusCodeResourceExhausted:  `"RESOURCE_EXHAUSTED"`,
-	trace.StatusCodeFailedPrecondition: `"FAILED_PRECONDITION"`,
-	trace.StatusCodeAborted:            `"ABORTED"`,
-	trace.StatusCodeOutOfRange:         `"OUT_OF_RANGE"`,
-	trace.StatusCodeUnimplemented:      `"UNIMPLEMENTED"`,
-	trace.StatusCodeInternal:           `"INTERNAL"`,
-	trace.StatusCodeUnavailable:        `"UNAVAILABLE"`,
-	trace.StatusCodeDataLoss:           `"DATA_LOSS"`,
-	trace.StatusCodeUnauthenticated:    `"UNAUTHENTICATED"`,
+	trace.StatusCodeOK:                 `OK`,
+	trace.StatusCodeCancelled:          `CANCELLED`,
+	trace.StatusCodeUnknown:            `UNKNOWN`,
+	trace.StatusCodeInvalidArgument:    `INVALID_ARGUMENT`,
+	trace.StatusCodeDeadlineExceeded:   `DEADLINE_EXCEEDED`,
+	trace.StatusCodeNotFound:           `NOT_FOUND`,
+	trace.StatusCodeAlreadyExists:      `ALREADY_EXISTS`,
+	trace.StatusCodePermissionDenied:   `PERMISSION_DENIED`,
+	trace.StatusCodeResourceExhausted:  `RESOURCE_EXHAUSTED`,
+	trace.StatusCodeFailedPrecondition: `FAILED_PRECONDITION`,
+	trace.StatusCodeAborted:            `ABORTED`,
+	trace.StatusCodeOutOfRange:         `OUT_OF_RANGE`,
+	trace.StatusCodeUnimplemented:      `UNIMPLEMENTED`,
+	trace.StatusCodeInternal:           `INTERNAL`,
+	trace.StatusCodeUnavailable:        `UNAVAILABLE`,
+	trace.StatusCodeDataLoss:           `DATA_LOSS`,
+	trace.StatusCodeUnauthenticated:    `UNAUTHENTICATED`,
 }
 
 func isHealthEndpoint(path string) bool {
diff --git a/resource/resource.go b/resource/resource.go
new file mode 100644
index 0000000..ec89b21
--- /dev/null
+++ b/resource/resource.go
@@ -0,0 +1,163 @@
+// 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 resource provides functionality for resource, which capture
+// identifying information about the entities for which signals are exported.
+package resource
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"regexp"
+	"sort"
+	"strconv"
+	"strings"
+)
+
+const (
+	EnvVarType   = "OC_RESOURCE_TYPE"
+	EnvVarLabels = "OC_RESOURCE_LABELS"
+)
+
+// Resource describes an entity about which identifying information and metadata is exposed.
+// For example, a type "k8s.io/container" may hold labels describing the pod name and namespace.
+type Resource struct {
+	Type   string
+	Labels map[string]string
+}
+
+// EncodeLabels encodes a labels map to a string as provided via the OC_RESOURCE_LABELS environment variable.
+func EncodeLabels(labels map[string]string) string {
+	sortedKeys := make([]string, 0, len(labels))
+	for k := range labels {
+		sortedKeys = append(sortedKeys, k)
+	}
+	sort.Strings(sortedKeys)
+
+	s := ""
+	for i, k := range sortedKeys {
+		if i > 0 {
+			s += ","
+		}
+		s += k + "=" + strconv.Quote(labels[k])
+	}
+	return s
+}
+
+var labelRegex = regexp.MustCompile(`^\s*([[:ascii:]]{1,256}?)=("[[:ascii:]]{0,256}?")\s*,`)
+
+// DecodeLabels decodes a serialized label map as used in the OC_RESOURCE_LABELS variable.
+// A list of labels of the form `<key1>="<value1>",<key2>="<value2>",...` is accepted.
+// Domain names and paths are accepted as label keys.
+// Most users will want to use FromEnv instead.
+func DecodeLabels(s string) (map[string]string, error) {
+	m := map[string]string{}
+	// Ensure a trailing comma, which allows us to keep the regex simpler
+	s = strings.TrimRight(strings.TrimSpace(s), ",") + ","
+
+	for len(s) > 0 {
+		match := labelRegex.FindStringSubmatch(s)
+		if len(match) == 0 {
+			return nil, fmt.Errorf("invalid label formatting, remainder: %s", s)
+		}
+		v := match[2]
+		if v == "" {
+			v = match[3]
+		} else {
+			var err error
+			if v, err = strconv.Unquote(v); err != nil {
+				return nil, fmt.Errorf("invalid label formatting, remainder: %s, err: %s", s, err)
+			}
+		}
+		m[match[1]] = v
+
+		s = s[len(match[0]):]
+	}
+	return m, nil
+}
+
+// FromEnv is a detector that loads resource information from the OC_RESOURCE_TYPE
+// and OC_RESOURCE_labelS environment variables.
+func FromEnv(context.Context) (*Resource, error) {
+	res := &Resource{
+		Type: strings.TrimSpace(os.Getenv(EnvVarType)),
+	}
+	labels := strings.TrimSpace(os.Getenv(EnvVarLabels))
+	if labels == "" {
+		return res, nil
+	}
+	var err error
+	if res.Labels, err = DecodeLabels(labels); err != nil {
+		return nil, err
+	}
+	return res, nil
+}
+
+var _ Detector = FromEnv
+
+// merge resource information from b into a. In case of a collision, a takes precedence.
+func merge(a, b *Resource) *Resource {
+	if a == nil {
+		return b
+	}
+	if b == nil {
+		return a
+	}
+	res := &Resource{
+		Type:   a.Type,
+		Labels: map[string]string{},
+	}
+	if res.Type == "" {
+		res.Type = b.Type
+	}
+	for k, v := range b.Labels {
+		res.Labels[k] = v
+	}
+	// Labels from resource a overwrite labels from resource b.
+	for k, v := range a.Labels {
+		res.Labels[k] = v
+	}
+	return res
+}
+
+// Detector attempts to detect resource information.
+// If the detector cannot find resource information, the returned resource is nil but no
+// error is returned.
+// An error is only returned on unexpected failures.
+type Detector func(context.Context) (*Resource, error)
+
+// MultiDetector returns a Detector that calls all input detectors in order and
+// merges each result with the previous one. In case a type of label key is already set,
+// the first set value is takes precedence.
+// It returns on the first error that a sub-detector encounters.
+func MultiDetector(detectors ...Detector) Detector {
+	return func(ctx context.Context) (*Resource, error) {
+		return detectAll(ctx, detectors...)
+	}
+}
+
+// detectall calls all input detectors sequentially an merges each result with the previous one.
+// It returns on the first error that a sub-detector encounters.
+func detectAll(ctx context.Context, detectors ...Detector) (*Resource, error) {
+	var res *Resource
+	for _, d := range detectors {
+		r, err := d(ctx)
+		if err != nil {
+			return nil, err
+		}
+		res = merge(res, r)
+	}
+	return res, nil
+}
diff --git a/stats/doc.go b/stats/doc.go
index 7a8a62c..00d473e 100644
--- a/stats/doc.go
+++ b/stats/doc.go
@@ -21,35 +21,49 @@ aggregate the collected data, and export the aggregated data.
 
 Measures
 
-A measure represents a type of metric to be tracked and recorded.
+A measure represents a type of data point to be tracked and recorded.
 For example, latency, request Mb/s, and response Mb/s are measures
 to collect from a server.
 
-Each measure needs to be registered before being used. Measure
-constructors such as Int64 and Float64 automatically
+Measure constructors such as Int64 and Float64 automatically
 register the measure by the given name. Each registered measure needs
 to be unique by name. Measures also have a description and a unit.
 
-Libraries can define and export measures for their end users to
-create views and collect instrumentation data.
+Libraries can define and export measures. Application authors can then
+create views and collect and break down measures by the tags they are
+interested in.
 
 Recording measurements
 
 Measurement is a data point to be collected for a measure. For example,
 for a latency (ms) measure, 100 is a measurement that represents a 100ms
-latency event. Users collect data points on the existing measures with
+latency event. Measurements are created from measures with
 the current context. Tags from the current context are recorded with the
 measurements if they are any.
 
-Recorded measurements are dropped immediately if user is not aggregating
-them via views. Users don't necessarily need to conditionally enable/disable
+Recorded measurements are dropped immediately if no views are registered for them.
+There is usually no need to conditionally enable and disable
 recording to reduce cost. Recording of measurements is cheap.
 
-Libraries can always record measurements, and end-users can later decide
+Libraries can always record measurements, and applications can later decide
 on which measurements they want to collect by registering views. This allows
 libraries to turn on the instrumentation by default.
+
+Exemplars
+
+For a given recorded measurement, the associated exemplar is a diagnostic map
+that gives more information about the measurement.
+
+When aggregated using a Distribution aggregation, an exemplar is kept for each
+bucket in the Distribution. This allows you to easily find an example of a
+measurement that fell into each bucket.
+
+For example, if you also use the OpenCensus trace package and you
+record a measurement with a context that contains a sampled trace span,
+then the trace span will be added to the exemplar associated with the measurement.
+
+When exported to a supporting back end, you should be able to easily navigate
+to example traces that fell into each bucket in the Distribution.
+
 */
 package stats // import "go.opencensus.io/stats"
-
-// TODO(acetechnologist): Add a link to the language independent OpenCensus
-// spec when it is available.
diff --git a/stats/internal/record.go b/stats/internal/record.go
index 6341eb2..ed54552 100644
--- a/stats/internal/record.go
+++ b/stats/internal/record.go
@@ -19,7 +19,7 @@ import (
 )
 
 // DefaultRecorder will be called for each Record call.
-var DefaultRecorder func(*tag.Map, interface{})
+var DefaultRecorder func(tags *tag.Map, measurement interface{}, attachments map[string]string)
 
 // SubscriptionReporter reports when a view subscribed with a measure.
 var SubscriptionReporter func(measure string)
diff --git a/stats/measure.go b/stats/measure.go
index 7b4b49c..64d02b1 100644
--- a/stats/measure.go
+++ b/stats/measure.go
@@ -68,6 +68,21 @@ func (m *measureDescriptor) subscribed() bool {
 	return atomic.LoadInt32(&m.subs) == 1
 }
 
+// Name returns the name of the measure.
+func (m *measureDescriptor) Name() string {
+	return m.name
+}
+
+// Description returns the description of the measure.
+func (m *measureDescriptor) Description() string {
+	return m.description
+}
+
+// Unit returns the unit of the measure.
+func (m *measureDescriptor) Unit() string {
+	return m.unit
+}
+
 var (
 	mu       sync.RWMutex
 	measures = make(map[string]*measureDescriptor)
@@ -94,7 +109,7 @@ func registerMeasureHandle(name, desc, unit string) *measureDescriptor {
 // provides M to convert an int64 into a measurement.
 type Measurement struct {
 	v float64
-	m Measure
+	m *measureDescriptor
 }
 
 // Value returns the value of the Measurement as a float64.
diff --git a/stats/measure_float64.go b/stats/measure_float64.go
index da4b5a8..acedb21 100644
--- a/stats/measure_float64.go
+++ b/stats/measure_float64.go
@@ -17,31 +17,13 @@ package stats
 
 // Float64Measure is a measure for float64 values.
 type Float64Measure struct {
-	md *measureDescriptor
-}
-
-// Name returns the name of the measure.
-func (m *Float64Measure) Name() string {
-	return m.md.name
-}
-
-// Description returns the description of the measure.
-func (m *Float64Measure) Description() string {
-	return m.md.description
-}
-
-// Unit returns the unit of the measure.
-func (m *Float64Measure) Unit() string {
-	return m.md.unit
+	*measureDescriptor
 }
 
 // M creates a new float64 measurement.
 // Use Record to record measurements.
 func (m *Float64Measure) M(v float64) Measurement {
-	if !m.md.subscribed() {
-		return Measurement{}
-	}
-	return Measurement{m: m, v: v}
+	return Measurement{m: m.measureDescriptor, v: v}
 }
 
 // Float64 creates a new measure for float64 values.
diff --git a/stats/measure_int64.go b/stats/measure_int64.go
index 5fedaad..c4243ba 100644
--- a/stats/measure_int64.go
+++ b/stats/measure_int64.go
@@ -17,31 +17,13 @@ package stats
 
 // Int64Measure is a measure for int64 values.
 type Int64Measure struct {
-	md *measureDescriptor
-}
-
-// Name returns the name of the measure.
-func (m *Int64Measure) Name() string {
-	return m.md.name
-}
-
-// Description returns the description of the measure.
-func (m *Int64Measure) Description() string {
-	return m.md.description
-}
-
-// Unit returns the unit of the measure.
-func (m *Int64Measure) Unit() string {
-	return m.md.unit
+	*measureDescriptor
 }
 
 // M creates a new int64 measurement.
 // Use Record to record measurements.
 func (m *Int64Measure) M(v int64) Measurement {
-	if !m.md.subscribed() {
-		return Measurement{}
-	}
-	return Measurement{m: m, v: float64(v)}
+	return Measurement{m: m.measureDescriptor, v: float64(v)}
 }
 
 // Int64 creates a new measure for int64 values.
diff --git a/stats/record.go b/stats/record.go
index 98865ff..0aced02 100644
--- a/stats/record.go
+++ b/stats/record.go
@@ -18,6 +18,7 @@ package stats
 import (
 	"context"
 
+	"go.opencensus.io/exemplar"
 	"go.opencensus.io/stats/internal"
 	"go.opencensus.io/tag"
 )
@@ -30,15 +31,19 @@ func init() {
 	}
 }
 
-// Record records one or multiple measurements with the same tags at once.
+// Record records one or multiple measurements with the same context at once.
 // If there are any tags in the context, measurements will be tagged with them.
 func Record(ctx context.Context, ms ...Measurement) {
+	recorder := internal.DefaultRecorder
+	if recorder == nil {
+		return
+	}
 	if len(ms) == 0 {
 		return
 	}
-	var record bool
+	record := false
 	for _, m := range ms {
-		if (m != Measurement{}) {
+		if m.m.subscribed() {
 			record = true
 			break
 		}
@@ -46,7 +51,19 @@ func Record(ctx context.Context, ms ...Measurement) {
 	if !record {
 		return
 	}
-	if internal.DefaultRecorder != nil {
-		internal.DefaultRecorder(tag.FromContext(ctx), ms)
+	recorder(tag.FromContext(ctx), ms, exemplar.AttachmentsFromContext(ctx))
+}
+
+// RecordWithTags records one or multiple measurements at once.
+//
+// Measurements will be tagged with the tags in the context mutated by the mutators.
+// RecordWithTags is useful if you want to record with tag mutations but don't want
+// to propagate the mutations in the context.
+func RecordWithTags(ctx context.Context, mutators []tag.Mutator, ms ...Measurement) error {
+	ctx, err := tag.New(ctx, mutators...)
+	if err != nil {
+		return err
 	}
+	Record(ctx, ms...)
+	return nil
 }
diff --git a/stats/view/aggregation_data.go b/stats/view/aggregation_data.go
index 88c500b..960b946 100644
--- a/stats/view/aggregation_data.go
+++ b/stats/view/aggregation_data.go
@@ -17,6 +17,8 @@ package view
 
 import (
 	"math"
+
+	"go.opencensus.io/exemplar"
 )
 
 // AggregationData represents an aggregated value from a collection.
@@ -24,7 +26,7 @@ import (
 // Mosts users won't directly access aggregration data.
 type AggregationData interface {
 	isAggregationData() bool
-	addSample(v float64)
+	addSample(e *exemplar.Exemplar)
 	clone() AggregationData
 	equal(other AggregationData) bool
 }
@@ -41,7 +43,7 @@ type CountData struct {
 
 func (a *CountData) isAggregationData() bool { return true }
 
-func (a *CountData) addSample(v float64) {
+func (a *CountData) addSample(_ *exemplar.Exemplar) {
 	a.Value = a.Value + 1
 }
 
@@ -68,8 +70,8 @@ type SumData struct {
 
 func (a *SumData) isAggregationData() bool { return true }
 
-func (a *SumData) addSample(f float64) {
-	a.Value += f
+func (a *SumData) addSample(e *exemplar.Exemplar) {
+	a.Value += e.Value
 }
 
 func (a *SumData) clone() AggregationData {
@@ -88,22 +90,30 @@ func (a *SumData) equal(other AggregationData) bool {
 // Distribution aggregation.
 //
 // Most users won't directly access distribution data.
+//
+// For a distribution with N bounds, the associated DistributionData will have
+// N+1 buckets.
 type DistributionData struct {
-	Count           int64     // number of data points aggregated
-	Min             float64   // minimum value in the distribution
-	Max             float64   // max value in the distribution
-	Mean            float64   // mean of the distribution
-	SumOfSquaredDev float64   // sum of the squared deviation from the mean
-	CountPerBucket  []int64   // number of occurrences per bucket
-	bounds          []float64 // histogram distribution of the values
+	Count           int64   // number of data points aggregated
+	Min             float64 // minimum value in the distribution
+	Max             float64 // max value in the distribution
+	Mean            float64 // mean of the distribution
+	SumOfSquaredDev float64 // sum of the squared deviation from the mean
+	CountPerBucket  []int64 // number of occurrences per bucket
+	// ExemplarsPerBucket is slice the same length as CountPerBucket containing
+	// an exemplar for the associated bucket, or nil.
+	ExemplarsPerBucket []*exemplar.Exemplar
+	bounds             []float64 // histogram distribution of the values
 }
 
 func newDistributionData(bounds []float64) *DistributionData {
+	bucketCount := len(bounds) + 1
 	return &DistributionData{
-		CountPerBucket: make([]int64, len(bounds)+1),
-		bounds:         bounds,
-		Min:            math.MaxFloat64,
-		Max:            math.SmallestNonzeroFloat64,
+		CountPerBucket:     make([]int64, bucketCount),
+		ExemplarsPerBucket: make([]*exemplar.Exemplar, bucketCount),
+		bounds:             bounds,
+		Min:                math.MaxFloat64,
+		Max:                math.SmallestNonzeroFloat64,
 	}
 }
 
@@ -119,7 +129,8 @@ func (a *DistributionData) variance() float64 {
 
 func (a *DistributionData) isAggregationData() bool { return true }
 
-func (a *DistributionData) addSample(f float64) {
+func (a *DistributionData) addSample(e *exemplar.Exemplar) {
+	f := e.Value
 	if f < a.Min {
 		a.Min = f
 	}
@@ -127,7 +138,7 @@ func (a *DistributionData) addSample(f float64) {
 		a.Max = f
 	}
 	a.Count++
-	a.incrementBucketCount(f)
+	a.addToBucket(e)
 
 	if a.Count == 1 {
 		a.Mean = f
@@ -139,26 +150,43 @@ func (a *DistributionData) addSample(f float64) {
 	a.SumOfSquaredDev = a.SumOfSquaredDev + (f-oldMean)*(f-a.Mean)
 }
 
-func (a *DistributionData) incrementBucketCount(f float64) {
-	if len(a.bounds) == 0 {
-		a.CountPerBucket[0]++
-		return
-	}
-
+func (a *DistributionData) addToBucket(e *exemplar.Exemplar) {
+	var count *int64
+	var ex **exemplar.Exemplar
 	for i, b := range a.bounds {
-		if f < b {
-			a.CountPerBucket[i]++
-			return
+		if e.Value < b {
+			count = &a.CountPerBucket[i]
+			ex = &a.ExemplarsPerBucket[i]
+			break
 		}
 	}
-	a.CountPerBucket[len(a.bounds)]++
+	if count == nil {
+		count = &a.CountPerBucket[len(a.bounds)]
+		ex = &a.ExemplarsPerBucket[len(a.bounds)]
+	}
+	*count++
+	*ex = maybeRetainExemplar(*ex, e)
+}
+
+func maybeRetainExemplar(old, cur *exemplar.Exemplar) *exemplar.Exemplar {
+	if old == nil {
+		return cur
+	}
+
+	// Heuristic to pick the "better" exemplar: first keep the one with a
+	// sampled trace attachment, if neither have a trace attachment, pick the
+	// one with more attachments.
+	_, haveTraceID := cur.Attachments[exemplar.KeyTraceID]
+	if haveTraceID || len(cur.Attachments) >= len(old.Attachments) {
+		return cur
+	}
+	return old
 }
 
 func (a *DistributionData) clone() AggregationData {
-	counts := make([]int64, len(a.CountPerBucket))
-	copy(counts, a.CountPerBucket)
 	c := *a
-	c.CountPerBucket = counts
+	c.CountPerBucket = append([]int64(nil), a.CountPerBucket...)
+	c.ExemplarsPerBucket = append([]*exemplar.Exemplar(nil), a.ExemplarsPerBucket...)
 	return &c
 }
 
@@ -190,8 +218,8 @@ func (l *LastValueData) isAggregationData() bool {
 	return true
 }
 
-func (l *LastValueData) addSample(v float64) {
-	l.Value = v
+func (l *LastValueData) addSample(e *exemplar.Exemplar) {
+	l.Value = e.Value
 }
 
 func (l *LastValueData) clone() AggregationData {
diff --git a/stats/view/collector.go b/stats/view/collector.go
index 250395d..32415d4 100644
--- a/stats/view/collector.go
+++ b/stats/view/collector.go
@@ -18,6 +18,8 @@ package view
 import (
 	"sort"
 
+	"go.opencensus.io/exemplar"
+
 	"go.opencensus.io/internal/tagencoding"
 	"go.opencensus.io/tag"
 )
@@ -31,13 +33,13 @@ type collector struct {
 	a *Aggregation
 }
 
-func (c *collector) addSample(s string, v float64) {
+func (c *collector) addSample(s string, e *exemplar.Exemplar) {
 	aggregator, ok := c.signatures[s]
 	if !ok {
 		aggregator = c.a.newData()
 		c.signatures[s] = aggregator
 	}
-	aggregator.addSample(v)
+	aggregator.addSample(e)
 }
 
 // collectRows returns a snapshot of the collected Row values.
diff --git a/stats/view/doc.go b/stats/view/doc.go
index 856fb4e..dced225 100644
--- a/stats/view/doc.go
+++ b/stats/view/doc.go
@@ -13,33 +13,34 @@
 // limitations under the License.
 //
 
-/*
-Package view contains support for collecting and exposing aggregates over stats.
-
-In order to collect measurements, views need to be defined and registered.
-A view allows recorded measurements to be filtered and aggregated over a time window.
-
-All recorded measurements can be filtered by a list of tags.
-
-OpenCensus provides several aggregation methods: count, distribution and sum.
-Count aggregation only counts the number of measurement points. Distribution
-aggregation provides statistical summary of the aggregated data. Sum distribution
-sums up the measurement points. Aggregations are cumulative.
-
-Users can dynamically create and delete views.
-
-Libraries can export their own views and claim the view names
-by registering them themselves.
-
-Exporting
-
-Collected and aggregated data can be exported to a metric collection
-backend by registering its exporter.
-
-Multiple exporters can be registered to upload the data to various
-different backends. Users need to unregister the exporters once they
-no longer are needed.
-*/
+// Package view contains support for collecting and exposing aggregates over stats.
+//
+// In order to collect measurements, views need to be defined and registered.
+// A view allows recorded measurements to be filtered and aggregated.
+//
+// All recorded measurements can be grouped by a list of tags.
+//
+// OpenCensus provides several aggregation methods: Count, Distribution and Sum.
+//
+// Count only counts the number of measurement points recorded.
+// Distribution provides statistical summary of the aggregated data by counting
+// how many recorded measurements fall into each bucket.
+// Sum adds up the measurement values.
+// LastValue just keeps track of the most recently recorded measurement value.
+// All aggregations are cumulative.
+//
+// Views can be registerd and unregistered at any time during program execution.
+//
+// Libraries can define views but it is recommended that in most cases registering
+// views be left up to applications.
+//
+// Exporting
+//
+// Collected and aggregated data can be exported to a metric collection
+// backend by registering its exporter.
+//
+// Multiple exporters can be registered to upload the data to various
+// different back ends.
 package view // import "go.opencensus.io/stats/view"
 
 // TODO(acetechnologist): Add a link to the language independent OpenCensus
diff --git a/stats/view/view.go b/stats/view/view.go
index 22323e2..c2a08af 100644
--- a/stats/view/view.go
+++ b/stats/view/view.go
@@ -23,6 +23,8 @@ import (
 	"sync/atomic"
 	"time"
 
+	"go.opencensus.io/exemplar"
+
 	"go.opencensus.io/stats"
 	"go.opencensus.io/stats/internal"
 	"go.opencensus.io/tag"
@@ -127,12 +129,12 @@ func (v *viewInternal) collectedRows() []*Row {
 	return v.collector.collectedRows(v.view.TagKeys)
 }
 
-func (v *viewInternal) addSample(m *tag.Map, val float64) {
+func (v *viewInternal) addSample(m *tag.Map, e *exemplar.Exemplar) {
 	if !v.isSubscribed() {
 		return
 	}
 	sig := string(encodeWithKeys(m, v.view.TagKeys))
-	v.collector.addSample(sig, val)
+	v.collector.addSample(sig, e)
 }
 
 // A Data is a set of rows about usage of the single measure associated
diff --git a/stats/view/worker.go b/stats/view/worker.go
index fef7bf5..63b0ee3 100644
--- a/stats/view/worker.go
+++ b/stats/view/worker.go
@@ -107,17 +107,23 @@ func RetrieveData(viewName string) ([]*Row, error) {
 	return resp.rows, resp.err
 }
 
-func record(tags *tag.Map, ms interface{}) {
+func record(tags *tag.Map, ms interface{}, attachments map[string]string) {
 	req := &recordReq{
-		tm: tags,
-		ms: ms.([]stats.Measurement),
+		tm:          tags,
+		ms:          ms.([]stats.Measurement),
+		attachments: attachments,
+		t:           time.Now(),
 	}
 	defaultWorker.c <- req
 }
 
 // SetReportingPeriod sets the interval between reporting aggregated views in
-// the program. If duration is less than or
-// equal to zero, it enables the default behavior.
+// the program. If duration is less than or equal to zero, it enables the
+// default behavior.
+//
+// Note: each exporter makes different promises about what the lowest supported
+// duration is. For example, the Stackdriver exporter recommends a value no
+// lower than 1 minute. Consult each exporter per your needs.
 func SetReportingPeriod(d time.Duration) {
 	// TODO(acetechnologist): ensure that the duration d is more than a certain
 	// value. e.g. 1s
diff --git a/stats/view/worker_commands.go b/stats/view/worker_commands.go
index 06c3c54..b38f26f 100644
--- a/stats/view/worker_commands.go
+++ b/stats/view/worker_commands.go
@@ -21,6 +21,8 @@ import (
 	"strings"
 	"time"
 
+	"go.opencensus.io/exemplar"
+
 	"go.opencensus.io/stats"
 	"go.opencensus.io/stats/internal"
 	"go.opencensus.io/tag"
@@ -140,8 +142,10 @@ func (cmd *retrieveDataReq) handleCommand(w *worker) {
 // recordReq is the command to record data related to multiple measures
 // at once.
 type recordReq struct {
-	tm *tag.Map
-	ms []stats.Measurement
+	tm          *tag.Map
+	ms          []stats.Measurement
+	attachments map[string]string
+	t           time.Time
 }
 
 func (cmd *recordReq) handleCommand(w *worker) {
@@ -151,7 +155,12 @@ func (cmd *recordReq) handleCommand(w *worker) {
 		}
 		ref := w.getMeasureRef(m.Measure().Name())
 		for v := range ref.views {
-			v.addSample(cmd.tm, m.Value())
+			e := &exemplar.Exemplar{
+				Value:       m.Value(),
+				Timestamp:   cmd.t,
+				Attachments: cmd.attachments,
+			}
+			v.addSample(cmd.tm, e)
 		}
 	}
 }
diff --git a/tag/context.go b/tag/context.go
index ed528bc..dcc13f4 100644
--- a/tag/context.go
+++ b/tag/context.go
@@ -15,7 +15,11 @@
 
 package tag
 
-import "context"
+import (
+	"context"
+
+	"go.opencensus.io/exemplar"
+)
 
 // FromContext returns the tag map stored in the context.
 func FromContext(ctx context.Context) *Map {
@@ -39,3 +43,25 @@ func NewContext(ctx context.Context, m *Map) context.Context {
 type ctxKey struct{}
 
 var mapCtxKey = ctxKey{}
+
+func init() {
+	exemplar.RegisterAttachmentExtractor(extractTagsAttachments)
+}
+
+func extractTagsAttachments(ctx context.Context, a exemplar.Attachments) exemplar.Attachments {
+	m := FromContext(ctx)
+	if m == nil {
+		return a
+	}
+	if len(m.m) == 0 {
+		return a
+	}
+	if a == nil {
+		a = make(map[string]string)
+	}
+
+	for k, v := range m.m {
+		a[exemplar.KeyPrefixTag+k.Name()] = v
+	}
+	return a
+}
diff --git a/trace/config.go b/trace/config.go
index e464671..0816892 100644
--- a/trace/config.go
+++ b/trace/config.go
@@ -15,8 +15,9 @@
 package trace
 
 import (
-	"go.opencensus.io/trace/internal"
 	"sync"
+
+	"go.opencensus.io/trace/internal"
 )
 
 // Config represents the global tracing configuration.
diff --git a/trace/doc.go b/trace/doc.go
index db00044..04b1ee4 100644
--- a/trace/doc.go
+++ b/trace/doc.go
@@ -32,6 +32,8 @@ to sample a subset of traces, or use AlwaysSample to collect a trace on every ru
 
     trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
 
+Be careful about using trace.AlwaysSample in a production application with
+significant traffic: a new trace will be started and exported for every request.
 
 Adding Spans to a Trace
 
diff --git a/trace/exemplar.go b/trace/exemplar.go
new file mode 100644
index 0000000..416d805
--- /dev/null
+++ b/trace/exemplar.go
@@ -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 trace
+
+import (
+	"context"
+	"encoding/hex"
+
+	"go.opencensus.io/exemplar"
+)
+
+func init() {
+	exemplar.RegisterAttachmentExtractor(attachSpanContext)
+}
+
+func attachSpanContext(ctx context.Context, a exemplar.Attachments) exemplar.Attachments {
+	span := FromContext(ctx)
+	if span == nil {
+		return a
+	}
+	sc := span.SpanContext()
+	if !sc.IsSampled() {
+		return a
+	}
+	if a == nil {
+		a = make(exemplar.Attachments)
+	}
+	a[exemplar.KeyTraceID] = hex.EncodeToString(sc.TraceID[:])
+	a[exemplar.KeySpanID] = hex.EncodeToString(sc.SpanID[:])
+	return a
+}
diff --git a/trace/sampling.go b/trace/sampling.go
index 313f8b6..71c10f9 100644
--- a/trace/sampling.go
+++ b/trace/sampling.go
@@ -20,10 +20,6 @@ import (
 
 const defaultSamplingProbability = 1e-4
 
-func newDefaultSampler() Sampler {
-	return ProbabilitySampler(defaultSamplingProbability)
-}
-
 // Sampler decides whether a trace should be sampled and exported.
 type Sampler func(SamplingParameters) SamplingDecision
 
@@ -62,6 +58,9 @@ func ProbabilitySampler(fraction float64) Sampler {
 }
 
 // AlwaysSample returns a Sampler that samples every trace.
+// Be careful about using this sampler in a production application with
+// significant traffic: a new trace will be started and exported for every
+// request.
 func AlwaysSample() Sampler {
 	return func(p SamplingParameters) SamplingDecision {
 		return SamplingDecision{Sample: true}
diff --git a/trace/trace.go b/trace/trace.go
index 887e90b..9e5e5f0 100644
--- a/trace/trace.go
+++ b/trace/trace.go
@@ -25,6 +25,7 @@ import (
 	"time"
 
 	"go.opencensus.io/internal"
+	"go.opencensus.io/trace/tracestate"
 )
 
 // Span represents a span of a trace.  It has an associated SpanContext, and
@@ -88,6 +89,7 @@ type SpanContext struct {
 	TraceID      TraceID
 	SpanID       SpanID
 	TraceOptions TraceOptions
+	Tracestate   *tracestate.Tracestate
 }
 
 type contextKey struct{}
@@ -241,13 +243,16 @@ func startSpanInternal(name string, hasParent bool, parent SpanContext, remotePa
 
 // End ends the span.
 func (s *Span) End() {
+	if s == nil {
+		return
+	}
+	if s.executionTracerTaskEnd != nil {
+		s.executionTracerTaskEnd()
+	}
 	if !s.IsRecordingEvents() {
 		return
 	}
 	s.endOnce.Do(func() {
-		if s.executionTracerTaskEnd != nil {
-			s.executionTracerTaskEnd()
-		}
 		exp, _ := exporters.Load().(exportersMap)
 		mustExport := s.spanContext.IsSampled() && len(exp) > 0
 		if s.spanStore != nil || mustExport {
diff --git a/trace/tracestate/tracestate.go b/trace/tracestate/tracestate.go
new file mode 100644
index 0000000..2d6c713
--- /dev/null
+++ b/trace/tracestate/tracestate.go
@@ -0,0 +1,147 @@
+// 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 tracestate implements support for the Tracestate header of the
+// W3C TraceContext propagation format.
+package tracestate
+
+import (
+	"fmt"
+	"regexp"
+)
+
+const (
+	keyMaxSize       = 256
+	valueMaxSize     = 256
+	maxKeyValuePairs = 32
+)
+
+const (
+	keyWithoutVendorFormat = `[a-z][_0-9a-z\-\*\/]{0,255}`
+	keyWithVendorFormat    = `[a-z][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}`
+	keyFormat              = `(` + keyWithoutVendorFormat + `)|(` + keyWithVendorFormat + `)`
+	valueFormat            = `[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]`
+)
+
+var keyValidationRegExp = regexp.MustCompile(`^(` + keyFormat + `)$`)
+var valueValidationRegExp = regexp.MustCompile(`^(` + valueFormat + `)$`)
+
+// Tracestate represents 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.
+type Tracestate struct {
+	entries []Entry
+}
+
+// Entry represents one key-value pair in a list of key-value pair of Tracestate.
+type Entry struct {
+	// Key is an 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 /.
+	Key string
+
+	// Value is an opaque string up to 256 characters printable ASCII RFC0020 characters (i.e., the
+	// range 0x20 to 0x7E) except comma , and =.
+	Value string
+}
+
+// Entries returns a slice of Entry.
+func (ts *Tracestate) Entries() []Entry {
+	if ts == nil {
+		return nil
+	}
+	return ts.entries
+}
+
+func (ts *Tracestate) remove(key string) *Entry {
+	for index, entry := range ts.entries {
+		if entry.Key == key {
+			ts.entries = append(ts.entries[:index], ts.entries[index+1:]...)
+			return &entry
+		}
+	}
+	return nil
+}
+
+func (ts *Tracestate) add(entries []Entry) error {
+	for _, entry := range entries {
+		ts.remove(entry.Key)
+	}
+	if len(ts.entries)+len(entries) > maxKeyValuePairs {
+		return fmt.Errorf("adding %d key-value pairs to current %d pairs exceeds the limit of %d",
+			len(entries), len(ts.entries), maxKeyValuePairs)
+	}
+	ts.entries = append(entries, ts.entries...)
+	return nil
+}
+
+func isValid(entry Entry) bool {
+	return keyValidationRegExp.MatchString(entry.Key) &&
+		valueValidationRegExp.MatchString(entry.Value)
+}
+
+func containsDuplicateKey(entries ...Entry) (string, bool) {
+	keyMap := make(map[string]int)
+	for _, entry := range entries {
+		if _, ok := keyMap[entry.Key]; ok {
+			return entry.Key, true
+		}
+		keyMap[entry.Key] = 1
+	}
+	return "", false
+}
+
+func areEntriesValid(entries ...Entry) (*Entry, bool) {
+	for _, entry := range entries {
+		if !isValid(entry) {
+			return &entry, false
+		}
+	}
+	return nil, true
+}
+
+// New creates a Tracestate object from a parent and/or entries (key-value pair).
+// Entries from the parent are copied if present. The entries passed to this function
+// are inserted in front of those copied from the parent. If an entry copied from the
+// parent contains the same key as one of the entry in entries then the entry copied
+// from the parent is removed. See add func.
+//
+// An error is returned with nil Tracestate if
+//  1. one or more entry in entries is invalid.
+//  2. two or more entries in the input entries have the same key.
+//  3. the number of entries combined from the parent and the input entries exceeds maxKeyValuePairs.
+//     (duplicate entry is counted only once).
+func New(parent *Tracestate, entries ...Entry) (*Tracestate, error) {
+	if parent == nil && len(entries) == 0 {
+		return nil, nil
+	}
+	if entry, ok := areEntriesValid(entries...); !ok {
+		return nil, fmt.Errorf("key-value pair {%s, %s} is invalid", entry.Key, entry.Value)
+	}
+
+	if key, duplicate := containsDuplicateKey(entries...); duplicate {
+		return nil, fmt.Errorf("contains duplicate keys (%s)", key)
+	}
+
+	tracestate := Tracestate{}
+
+	if parent != nil && len(parent.entries) > 0 {
+		tracestate.entries = append([]Entry{}, parent.entries...)
+	}
+
+	err := tracestate.add(entries)
+	if err != nil {
+		return nil, err
+	}
+	return &tracestate, nil
+}
diff --git a/vendor/git.apache.org/thrift.git/tutorial/erl/server.sh b/vendor/git.apache.org/thrift.git/tutorial/erl/server.sh
deleted file mode 120000
index 26b3c58..0000000
--- a/vendor/git.apache.org/thrift.git/tutorial/erl/server.sh
+++ /dev/null
@@ -1,1 +0,0 @@
-client.sh
\ No newline at end of file
diff --git a/exporterutil/version.go b/zpages/formatter_test.go
similarity index 56%
rename from exporterutil/version.go
rename to zpages/formatter_test.go
index 8572061..92f5a8f 100644
--- a/exporterutil/version.go
+++ b/zpages/formatter_test.go
@@ -11,17 +11,32 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-
-// Package exporterutil contains common utilities for exporter implementations.
 //
-// Deprecated: Don't use this package.
-package exporterutil
 
-import "go.opencensus.io"
+package zpages
+
+import "testing"
 
-// Version is the current release version of OpenCensus in use. It is made
-// available for exporters to include in User-Agent-like metadata.
-// Deprecated: Use opencensus.Version().
-var Version = opencensus.Version()
+func TestCountFormatter(t *testing.T) {
+	tests := []struct {
+		in   uint64
+		want string
+	}{
+		{0, " "},
+		{1, "1"},
+		{1024, "1024"},
+		{1e5, "100000"},
+		{1e6, "1.000 M "},
+		{1e9, "1.000 G "},
+		{1e8 + 2e9, "2.100 G "},
+		{1e12, "1.000 T "},
+		{1e15, "1.000 P "},
+		{1e18, "1.000 E "},
+	}
 
-// TODO(jbd): Remove this package at the next release.
+	for _, tt := range tests {
+		if g, w := countFormatter(tt.in), tt.want; g != w {
+			t.Errorf("%d got %q want %q", tt.in, g, w)
+		}
+	}
+}
diff --git a/zpages/rpcz.go b/zpages/rpcz.go
index 30193d1..dee28f9 100644
--- a/zpages/rpcz.go
+++ b/zpages/rpcz.go
@@ -170,9 +170,9 @@ type statSnapshot struct {
 	// TODO: compute hour/minute values from cumulative
 	Method           string
 	Received         bool
-	CountMinute      int
-	CountHour        int
-	CountTotal       int
+	CountMinute      uint64
+	CountHour        uint64
+	CountTotal       uint64
 	AvgLatencyMinute time.Duration
 	AvgLatencyHour   time.Duration
 	AvgLatencyTotal  time.Duration
@@ -185,9 +185,9 @@ type statSnapshot struct {
 	OutputRateMinute float64
 	OutputRateHour   float64
 	OutputRateTotal  float64
-	ErrorsMinute     int
-	ErrorsHour       int
-	ErrorsTotal      int
+	ErrorsMinute     uint64
+	ErrorsHour       uint64
+	ErrorsTotal      uint64
 }
 
 type methodKey struct {
@@ -267,7 +267,7 @@ func (s snapExporter) ExportView(vd *view.Data) {
 			}
 			for _, tag := range row.Tags {
 				if tag.Key == ocgrpc.KeyClientStatus && tag.Value != "OK" {
-					s.ErrorsTotal += int(count)
+					s.ErrorsTotal += uint64(count)
 				}
 			}
 
@@ -281,7 +281,7 @@ func (s snapExporter) ExportView(vd *view.Data) {
 			s.InputRateTotal = computeRate(0, sum)
 
 		case ocgrpc.ClientSentMessagesPerRPCView:
-			s.CountTotal = int(count)
+			s.CountTotal = uint64(count)
 			s.RPCRateTotal = computeRate(0, count)
 
 		case ocgrpc.ClientReceivedMessagesPerRPCView:
@@ -294,7 +294,7 @@ func (s snapExporter) ExportView(vd *view.Data) {
 			}
 			for _, tag := range row.Tags {
 				if tag.Key == ocgrpc.KeyServerStatus && tag.Value != "OK" {
-					s.ErrorsTotal += int(count)
+					s.ErrorsTotal += uint64(count)
 				}
 			}
 
@@ -305,7 +305,7 @@ func (s snapExporter) ExportView(vd *view.Data) {
 			s.OutputRateTotal = computeRate(0, sum)
 
 		case ocgrpc.ServerReceivedMessagesPerRPCView:
-			s.CountTotal = int(count)
+			s.CountTotal = uint64(count)
 			s.RPCRateTotal = computeRate(0, count)
 
 		case ocgrpc.ServerSentMessagesPerRPCView:
diff --git a/zpages/templates.go b/zpages/templates.go
index d60a12d..6675b0a 100644
--- a/zpages/templates.go
+++ b/zpages/templates.go
@@ -57,14 +57,21 @@ func parseTemplate(name string) *template.Template {
 	return template.Must(template.New(name).Funcs(templateFunctions).Parse(string(text)))
 }
 
-func countFormatter(num int) string {
-	if num == 0 {
+func countFormatter(num uint64) string {
+	if num <= 0 {
 		return " "
 	}
 	var floatVal float64
 	var suffix string
-	if num >= 1e12 {
-		floatVal = float64(num) / 1e9
+
+	if num >= 1e18 {
+		floatVal = float64(num) / 1e18
+		suffix = " E "
+	} else if num >= 1e15 {
+		floatVal = float64(num) / 1e15
+		suffix = " P "
+	} else if num >= 1e12 {
+		floatVal = float64(num) / 1e12
 		suffix = " T "
 	} else if num >= 1e9 {
 		floatVal = float64(num) / 1e9


Test patch:
diff --git a/exporter/jaeger/example_test.go b/exporter/jaeger/example_test.go
index 7865d6e..bb21207 100644
--- a/exporter/jaeger/example_test.go
+++ b/exporter/jaeger/example_test.go
@@ -25,8 +25,10 @@ func ExampleNewExporter_collector() {
 	// Register the Jaeger exporter to be able to retrieve
 	// the collected spans.
 	exporter, err := jaeger.NewExporter(jaeger.Options{
-		Endpoint:    "http://localhost:14268",
-		ServiceName: "trace-demo",
+		Endpoint: "http://localhost:14268",
+		Process: jaeger.Process{
+			ServiceName: "trace-demo",
+		},
 	})
 	if err != nil {
 		log.Fatal(err)
@@ -39,7 +41,31 @@ func ExampleNewExporter_agent() {
 	// the collected spans.
 	exporter, err := jaeger.NewExporter(jaeger.Options{
 		AgentEndpoint: "localhost:6831",
-		ServiceName:   "trace-demo",
+		Process: jaeger.Process{
+			ServiceName: "trace-demo",
+		},
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+	trace.RegisterExporter(exporter)
+}
+
+// ExampleNewExporter_processTags shows how to set ProcessTags
+// on a Jaeger exporter. These tags will be added to the exported
+// Jaeger process.
+func ExampleNewExporter_processTags() {
+	// Register the Jaeger exporter to be able to retrieve
+	// the collected spans.
+	exporter, err := jaeger.NewExporter(jaeger.Options{
+		AgentEndpoint: "localhost:6831",
+		Process: jaeger.Process{
+			ServiceName: "trace-demo",
+			Tags: []jaeger.Tag{
+				jaeger.StringTag("ip", "127.0.0.1"),
+				jaeger.BoolTag("demo", true),
+			},
+		},
 	})
 	if err != nil {
 		log.Fatal(err)
diff --git a/exporter/prometheus/prometheus_test.go b/exporter/prometheus/prometheus_test.go
index 371da56..6998520 100644
--- a/exporter/prometheus/prometheus_test.go
+++ b/exporter/prometheus/prometheus_test.go
@@ -307,7 +307,7 @@ func TestCumulativenessFromHistograms(t *testing.T) {
 	}
 
 	ctx := context.Background()
-	ms := make([]stats.Measurement, len(values))
+	ms := make([]stats.Measurement, 0, len(values))
 	for _, value := range values {
 		mx := m.M(value)
 		ms = append(ms, mx)
diff --git a/exporter/zipkin/zipkin_test.go b/exporter/zipkin/zipkin_test.go
index 24194e0..2d5f81c 100644
--- a/exporter/zipkin/zipkin_test.go
+++ b/exporter/zipkin/zipkin_test.go
@@ -110,11 +110,11 @@ func TestExport(t *testing.T) {
 					},
 				},
 				Tags: map[string]string{
-					"stringkey": "value",
-					"intkey":    "42",
-					"boolkey1":  "true",
-					"boolkey2":  "false",
-					"error":     "INVALID_ARGUMENT",
+					"stringkey":                     "value",
+					"intkey":                        "42",
+					"boolkey1":                      "true",
+					"boolkey2":                      "false",
+					"error":                         "INVALID_ARGUMENT",
 					"opencensus.status_description": "error",
 				},
 			},
diff --git a/plugin/ocgrpc/server_stats_handler_test.go b/plugin/ocgrpc/server_stats_handler_test.go
index 3837758..1b96edf 100644
--- a/plugin/ocgrpc/server_stats_handler_test.go
+++ b/plugin/ocgrpc/server_stats_handler_test.go
@@ -324,10 +324,6 @@ func TestServerDefaultCollections(t *testing.T) {
 	}
 }
 
-func newCountData(v int) *view.CountData {
-	return &view.CountData{Value: int64(v)}
-}
-
 func newDistributionData(countPerBucket []int64, count int64, min, max, mean, sumOfSquaredDev float64) *view.DistributionData {
 	return &view.DistributionData{
 		Count:           count,
diff --git a/plugin/ochttp/client_test.go b/plugin/ochttp/client_test.go
index 5bed75f..97d15ab 100644
--- a/plugin/ochttp/client_test.go
+++ b/plugin/ochttp/client_test.go
@@ -30,7 +30,104 @@ import (
 
 const reqCount = 5
 
-func TestClient(t *testing.T) {
+func TestClientNew(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+		resp.Write([]byte("Hello, world!"))
+	}))
+	defer server.Close()
+
+	if err := view.Register(
+		ochttp.ClientSentBytesDistribution,
+		ochttp.ClientReceivedBytesDistribution,
+		ochttp.ClientRoundtripLatencyDistribution,
+		ochttp.ClientCompletedCount,
+	); err != nil {
+		t.Fatalf("Failed to register ochttp.DefaultClientViews error: %v", err)
+	}
+
+	views := []string{
+		"opencensus.io/http/client/sent_bytes",
+		"opencensus.io/http/client/received_bytes",
+		"opencensus.io/http/client/roundtrip_latency",
+		"opencensus.io/http/client/completed_count",
+	}
+	for _, name := range views {
+		v := view.Find(name)
+		if v == nil {
+			t.Errorf("view not found %q", name)
+			continue
+		}
+	}
+
+	var wg sync.WaitGroup
+	var tr ochttp.Transport
+	errs := make(chan error, reqCount)
+	wg.Add(reqCount)
+
+	for i := 0; i < reqCount; i++ {
+		go func() {
+			defer wg.Done()
+			req, err := http.NewRequest("POST", server.URL, strings.NewReader("req-body"))
+			if err != nil {
+				errs <- fmt.Errorf("error creating request: %v", err)
+			}
+			resp, err := tr.RoundTrip(req)
+			if err != nil {
+				errs <- fmt.Errorf("response error: %v", err)
+			}
+			if err := resp.Body.Close(); err != nil {
+				errs <- fmt.Errorf("error closing response body: %v", err)
+			}
+			if got, want := resp.StatusCode, 200; got != want {
+				errs <- fmt.Errorf("resp.StatusCode=%d; wantCount %d", got, want)
+			}
+		}()
+	}
+
+	go func() {
+		wg.Wait()
+		close(errs)
+	}()
+
+	for err := range errs {
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	for _, viewName := range views {
+		v := view.Find(viewName)
+		if v == nil {
+			t.Errorf("view not found %q", viewName)
+			continue
+		}
+		rows, err := view.RetrieveData(v.Name)
+		if err != nil {
+			t.Error(err)
+			continue
+		}
+		if got, want := len(rows), 1; got != want {
+			t.Errorf("len(%q) = %d; want %d", viewName, got, want)
+			continue
+		}
+		data := rows[0].Data
+		var count int64
+		switch data := data.(type) {
+		case *view.CountData:
+			count = data.Value
+		case *view.DistributionData:
+			count = data.Count
+		default:
+			t.Errorf("Unkown data type: %v", data)
+			continue
+		}
+		if got := count; got != reqCount {
+			t.Fatalf("%s = %d; want %d", viewName, got, reqCount)
+		}
+	}
+}
+
+func TestClientOld(t *testing.T) {
 	server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
 		resp.Write([]byte("Hello, world!"))
 	}))
diff --git a/plugin/ochttp/example_test.go b/plugin/ochttp/example_test.go
index 7a0c452..bb115ab 100644
--- a/plugin/ochttp/example_test.go
+++ b/plugin/ochttp/example_test.go
@@ -25,17 +25,21 @@ import (
 )
 
 func ExampleTransport() {
-	if err := view.Register(
-		// Register to a few default views.
-		ochttp.ClientRequestCountByMethod,
-		ochttp.ClientResponseCountByStatusCode,
-		ochttp.ClientLatencyView,
+	// import (
+	// 		"go.opencensus.io/plugin/ochttp"
+	//		"go.opencensus.io/stats/view"
+	// )
 
-		// Register to a custom view.
+	if err := view.Register(
+		// Register a few default views.
+		ochttp.ClientSentBytesDistribution,
+		ochttp.ClientReceivedBytesDistribution,
+		ochttp.ClientRoundtripLatencyDistribution,
+		// Register a custom view.
 		&view.View{
-			Name:        "httpclient_latency_by_hostpath",
-			TagKeys:     []tag.Key{ochttp.Host, ochttp.Path},
-			Measure:     ochttp.ClientLatency,
+			Name:        "httpclient_latency_by_path",
+			TagKeys:     []tag.Key{ochttp.KeyClientPath},
+			Measure:     ochttp.ClientRoundtripLatency,
 			Aggregation: ochttp.DefaultLatencyDistribution,
 		},
 	); err != nil {
@@ -45,22 +49,27 @@ func ExampleTransport() {
 	client := &http.Client{
 		Transport: &ochttp.Transport{},
 	}
-	_ = client // use client to perform requests
+
+	// Use client to perform requests.
+	_ = client
 }
 
 var usersHandler http.Handler
 
 func ExampleHandler() {
-	// Enables OpenCensus for the default serve mux.
-	// By default, B3 propagation is used.
-	http.Handle("/users", usersHandler)
+	// import "go.opencensus.io/plugin/ochttp"
+
+	http.Handle("/users", ochttp.WithRouteTag(usersHandler, "/users"))
+
+	// If no handler is specified, the default mux is used.
 	log.Fatal(http.ListenAndServe("localhost:8080", &ochttp.Handler{}))
 }
 
 func ExampleHandler_mux() {
-	mux := http.NewServeMux()
-	mux.Handle("/users", usersHandler)
+	// import "go.opencensus.io/plugin/ochttp"
 
+	mux := http.NewServeMux()
+	mux.Handle("/users", ochttp.WithRouteTag(usersHandler, "/users"))
 	log.Fatal(http.ListenAndServe("localhost:8080", &ochttp.Handler{
 		Handler:     mux,
 		Propagation: &b3.HTTPFormat{},
diff --git a/plugin/ochttp/propagation/tracecontext/propagation_test.go b/plugin/ochttp/propagation/tracecontext/propagation_test.go
index a9f0276..996cfa8 100644
--- a/plugin/ochttp/propagation/tracecontext/propagation_test.go
+++ b/plugin/ochttp/propagation/tracecontext/propagation_test.go
@@ -15,11 +15,29 @@
 package tracecontext
 
 import (
+	"fmt"
 	"net/http"
 	"reflect"
+	"strings"
 	"testing"
 
 	"go.opencensus.io/trace"
+	"go.opencensus.io/trace/tracestate"
+)
+
+var (
+	tpHeader        = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
+	traceID         = trace.TraceID{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54}
+	spanID          = trace.SpanID{0, 240, 103, 170, 11, 169, 2, 183}
+	traceOpt        = trace.TraceOptions(1)
+	oversizeValue   = strings.Repeat("a", maxTracestateLen/2)
+	oversizeEntry1  = tracestate.Entry{Key: "foo", Value: oversizeValue}
+	oversizeEntry2  = tracestate.Entry{Key: "hello", Value: oversizeValue}
+	entry1          = tracestate.Entry{Key: "foo", Value: "bar"}
+	entry2          = tracestate.Entry{Key: "hello", Value: "world   example"}
+	oversizeTs, _   = tracestate.New(nil, oversizeEntry1, oversizeEntry2)
+	defaultTs, _    = tracestate.New(nil, nil...)
+	nonDefaultTs, _ = tracestate.New(nil, entry1, entry2)
 )
 
 func TestHTTPFormat_FromRequest(t *testing.T) {
@@ -30,10 +48,14 @@ func TestHTTPFormat_FromRequest(t *testing.T) {
 		wantOk bool
 	}{
 		{
-			name:   "unsupported version",
+			name:   "future version",
 			header: "02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
-			wantSc: trace.SpanContext{},
-			wantOk: false,
+			wantSc: trace.SpanContext{
+				TraceID:      trace.TraceID{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54},
+				SpanID:       trace.SpanID{0, 240, 103, 170, 11, 169, 2, 183},
+				TraceOptions: trace.TraceOptions(1),
+			},
+			wantOk: true,
 		},
 		{
 			name:   "zero trace ID and span ID",
@@ -52,17 +74,13 @@ func TestHTTPFormat_FromRequest(t *testing.T) {
 			wantOk: true,
 		},
 		{
-			name:   "no options",
+			name:   "missing options",
 			header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7",
-			wantSc: trace.SpanContext{
-				TraceID:      trace.TraceID{75, 249, 47, 53, 119, 179, 77, 166, 163, 206, 146, 157, 14, 14, 71, 54},
-				SpanID:       trace.SpanID{0, 240, 103, 170, 11, 169, 2, 183},
-				TraceOptions: trace.TraceOptions(0),
-			},
-			wantOk: true,
+			wantSc: trace.SpanContext{},
+			wantOk: false,
 		},
 		{
-			name:   "missing options",
+			name:   "empty options",
 			header: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-",
 			wantSc: trace.SpanContext{},
 			wantOk: false,
@@ -113,3 +131,137 @@ func TestHTTPFormat_ToRequest(t *testing.T) {
 		})
 	}
 }
+
+func TestHTTPFormatTracestate_FromRequest(t *testing.T) {
+	scWithNonDefaultTracestate := trace.SpanContext{
+		TraceID:      traceID,
+		SpanID:       spanID,
+		TraceOptions: traceOpt,
+		Tracestate:   nonDefaultTs,
+	}
+
+	scWithDefaultTracestate := trace.SpanContext{
+		TraceID:      traceID,
+		SpanID:       spanID,
+		TraceOptions: traceOpt,
+		Tracestate:   defaultTs,
+	}
+
+	tests := []struct {
+		name     string
+		tpHeader string
+		tsHeader string
+		wantSc   trace.SpanContext
+		wantOk   bool
+	}{
+		{
+			name:     "tracestate invalid entries delimiter",
+			tpHeader: tpHeader,
+			tsHeader: "foo=bar;hello=world",
+			wantSc:   scWithDefaultTracestate,
+			wantOk:   true,
+		},
+		{
+			name:     "tracestate invalid key-value delimiter",
+			tpHeader: tpHeader,
+			tsHeader: "foo=bar,hello-world",
+			wantSc:   scWithDefaultTracestate,
+			wantOk:   true,
+		},
+		{
+			name:     "tracestate invalid value character",
+			tpHeader: tpHeader,
+			tsHeader: "foo=bar,hello=world   example   \u00a0  ",
+			wantSc:   scWithDefaultTracestate,
+			wantOk:   true,
+		},
+		{
+			name:     "tracestate blank key-value",
+			tpHeader: tpHeader,
+			tsHeader: "foo=bar,    ",
+			wantSc:   scWithDefaultTracestate,
+			wantOk:   true,
+		},
+		{
+			name:     "tracestate oversize header",
+			tpHeader: tpHeader,
+			tsHeader: fmt.Sprintf("foo=%s,hello=%s", oversizeValue, oversizeValue),
+			wantSc:   scWithDefaultTracestate,
+			wantOk:   true,
+		},
+		{
+			name:     "tracestate valid",
+			tpHeader: tpHeader,
+			tsHeader: "foo=bar   ,   hello=world   example",
+			wantSc:   scWithNonDefaultTracestate,
+			wantOk:   true,
+		},
+	}
+
+	f := &HTTPFormat{}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req, _ := http.NewRequest("GET", "http://example.com", nil)
+			req.Header.Set("traceparent", tt.tpHeader)
+			req.Header.Set("tracestate", tt.tsHeader)
+
+			gotSc, gotOk := f.SpanContextFromRequest(req)
+			if !reflect.DeepEqual(gotSc, tt.wantSc) {
+				t.Errorf("HTTPFormat.FromRequest() gotTs = %v, want %v", gotSc.Tracestate, tt.wantSc.Tracestate)
+			}
+			if gotOk != tt.wantOk {
+				t.Errorf("HTTPFormat.FromRequest() gotOk = %v, want %v", gotOk, tt.wantOk)
+			}
+		})
+	}
+}
+
+func TestHTTPFormatTracestate_ToRequest(t *testing.T) {
+	tests := []struct {
+		name       string
+		sc         trace.SpanContext
+		wantHeader string
+	}{
+		{
+			name: "valid span context with default tracestate",
+			sc: trace.SpanContext{
+				TraceID:      traceID,
+				SpanID:       spanID,
+				TraceOptions: traceOpt,
+			},
+			wantHeader: "",
+		},
+		{
+			name: "valid span context with non default tracestate",
+			sc: trace.SpanContext{
+				TraceID:      traceID,
+				SpanID:       spanID,
+				TraceOptions: traceOpt,
+				Tracestate:   nonDefaultTs,
+			},
+			wantHeader: "foo=bar,hello=world   example",
+		},
+		{
+			name: "valid span context with oversize tracestate",
+			sc: trace.SpanContext{
+				TraceID:      traceID,
+				SpanID:       spanID,
+				TraceOptions: traceOpt,
+				Tracestate:   oversizeTs,
+			},
+			wantHeader: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			f := &HTTPFormat{}
+			req, _ := http.NewRequest("GET", "http://example.com", nil)
+			f.SpanContextToRequest(tt.sc, req)
+
+			h := req.Header.Get("tracestate")
+			if got, want := h, tt.wantHeader; got != want {
+				t.Errorf("HTTPFormat.ToRequest() tracestate header = %v, want %v", got, want)
+			}
+		})
+	}
+}
diff --git a/plugin/ochttp/propagation_test.go b/plugin/ochttp/propagation_test.go
index f4645db..9ebfd89 100644
--- a/plugin/ochttp/propagation_test.go
+++ b/plugin/ochttp/propagation_test.go
@@ -36,8 +36,7 @@ func TestRoundTripAllFormats(t *testing.T) {
 	}
 
 	ctx := context.Background()
-	trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
-	ctx, span := trace.StartSpan(ctx, "test")
+	ctx, span := trace.StartSpan(ctx, "test", trace.WithSampler(trace.AlwaysSample()))
 	sc := span.SpanContext()
 	wantStr := fmt.Sprintf("trace_id=%x, span_id=%x, options=%d", sc.TraceID, sc.SpanID, sc.TraceOptions)
 	defer span.End()
diff --git a/plugin/ochttp/route_test.go b/plugin/ochttp/route_test.go
new file mode 100644
index 0000000..a9793eb
--- /dev/null
+++ b/plugin/ochttp/route_test.go
@@ -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 ochttp_test
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"go.opencensus.io/plugin/ochttp"
+	"go.opencensus.io/stats/view"
+	"go.opencensus.io/tag"
+)
+
+func TestWithRouteTag(t *testing.T) {
+	v := &view.View{
+		Name:        "request_total",
+		Measure:     ochttp.ServerLatency,
+		Aggregation: view.Count(),
+		TagKeys:     []tag.Key{ochttp.KeyServerRoute},
+	}
+	view.Register(v)
+	var e testStatsExporter
+	view.RegisterExporter(&e)
+	defer view.UnregisterExporter(&e)
+
+	mux := http.NewServeMux()
+	handler := ochttp.WithRouteTag(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(204)
+	}), "/a/")
+	mux.Handle("/a/", handler)
+	plugin := ochttp.Handler{Handler: mux}
+	req, _ := http.NewRequest("GET", "/a/b/c", nil)
+	rr := httptest.NewRecorder()
+	plugin.ServeHTTP(rr, req)
+	if got, want := rr.Code, 204; got != want {
+		t.Fatalf("Unexpected response, got %d; want %d", got, want)
+	}
+
+	view.Unregister(v) // trigger exporting
+
+	got := e.rowsForView("request_total")
+	want := []*view.Row{
+		{Data: &view.CountData{Value: 1}, Tags: []tag.Tag{{Key: ochttp.KeyServerRoute, Value: "/a/"}}},
+	}
+	if diff := cmp.Diff(got, want); diff != "" {
+		t.Errorf("Unexpected view data exported, -got, +want: %s", diff)
+	}
+}
+
+type testStatsExporter struct {
+	vd []*view.Data
+}
+
+func (t *testStatsExporter) ExportView(d *view.Data) {
+	t.vd = append(t.vd, d)
+}
+
+func (t *testStatsExporter) rowsForView(name string) []*view.Row {
+	var rows []*view.Row
+	for _, d := range t.vd {
+		if d.View.Name == name {
+			rows = append(rows, d.Rows...)
+		}
+	}
+	return rows
+}
diff --git a/plugin/ochttp/server_test.go b/plugin/ochttp/server_test.go
index 29ba795..29468cb 100644
--- a/plugin/ochttp/server_test.go
+++ b/plugin/ochttp/server_test.go
@@ -65,11 +65,14 @@ func TestHandlerStatsCollection(t *testing.T) {
 			body := bytes.NewBuffer(make([]byte, test.reqSize))
 			r := httptest.NewRequest(test.method, test.target, body)
 			w := httptest.NewRecorder()
+			mux := http.NewServeMux()
+			mux.Handle("/request/", httpHandler(test.statusCode, test.respSize))
 			h := &Handler{
-				Handler: httpHandler(test.statusCode, test.respSize),
+				Handler: mux,
+				StartOptions: trace.StartOptions{
+					Sampler: trace.NeverSample(),
+				},
 			}
-			h.StartOptions.Sampler = trace.NeverSample()
-
 			for i := 0; i < test.count; i++ {
 				h.ServeHTTP(w, r)
 				totalCount++
@@ -287,11 +290,11 @@ func TestEnsureTrackingResponseWriterSetsStatusCode(t *testing.T) {
 		res  *http.Response
 		want trace.Status
 	}{
-		{res: &http.Response{StatusCode: 200}, want: trace.Status{Code: trace.StatusCodeOK, Message: `"OK"`}},
-		{res: &http.Response{StatusCode: 500}, want: trace.Status{Code: trace.StatusCodeUnknown, Message: `"UNKNOWN"`}},
-		{res: &http.Response{StatusCode: 403}, want: trace.Status{Code: trace.StatusCodePermissionDenied, Message: `"PERMISSION_DENIED"`}},
-		{res: &http.Response{StatusCode: 401}, want: trace.Status{Code: trace.StatusCodeUnauthenticated, Message: `"UNAUTHENTICATED"`}},
-		{res: &http.Response{StatusCode: 429}, want: trace.Status{Code: trace.StatusCodeResourceExhausted, Message: `"RESOURCE_EXHAUSTED"`}},
+		{res: &http.Response{StatusCode: 200}, want: trace.Status{Code: trace.StatusCodeOK, Message: `OK`}},
+		{res: &http.Response{StatusCode: 500}, want: trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}},
+		{res: &http.Response{StatusCode: 403}, want: trace.Status{Code: trace.StatusCodePermissionDenied, Message: `PERMISSION_DENIED`}},
+		{res: &http.Response{StatusCode: 401}, want: trace.Status{Code: trace.StatusCodeUnauthenticated, Message: `UNAUTHENTICATED`}},
+		{res: &http.Response{StatusCode: 429}, want: trace.Status{Code: trace.StatusCodeResourceExhausted, Message: `RESOURCE_EXHAUSTED`}},
 	}
 
 	for _, tt := range tests {
diff --git a/plugin/ochttp/span_annotator_test.go b/plugin/ochttp/span_annotating_client_trace_test.go
similarity index 90%
rename from plugin/ochttp/span_annotator_test.go
rename to plugin/ochttp/span_annotating_client_trace_test.go
index 2642d94..3336d7e 100644
--- a/plugin/ochttp/span_annotator_test.go
+++ b/plugin/ochttp/span_annotating_client_trace_test.go
@@ -26,7 +26,7 @@ import (
 	"go.opencensus.io/trace"
 )
 
-func TestSpanAnnotator(t *testing.T) {
+func TestSpanAnnotatingClientTrace(t *testing.T) {
 	server := httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
 		resp.Write([]byte("Hello, world!"))
 	}))
@@ -36,7 +36,12 @@ func TestSpanAnnotator(t *testing.T) {
 
 	trace.RegisterExporter(recorder)
 
-	tr := ochttp.Transport{NewClientTrace: ochttp.NewSpanAnnotator}
+	tr := ochttp.Transport{
+		NewClientTrace: ochttp.NewSpanAnnotatingClientTrace,
+		StartOptions: trace.StartOptions{
+			Sampler: trace.AlwaysSample(),
+		},
+	}
 
 	req, err := http.NewRequest("POST", server.URL, strings.NewReader("req-body"))
 	if err != nil {
@@ -55,7 +60,7 @@ func TestSpanAnnotator(t *testing.T) {
 	}
 
 	if got, want := len(recorder.spans), 1; got != want {
-		t.Errorf("span count=%d; want=%d", got, want)
+		t.Fatalf("span count=%d; want=%d", got, want)
 	}
 
 	var annotations []string
diff --git a/plugin/ochttp/stats_test.go b/plugin/ochttp/stats_test.go
new file mode 100644
index 0000000..ce3621b
--- /dev/null
+++ b/plugin/ochttp/stats_test.go
@@ -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 ochttp
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+
+	"go.opencensus.io/stats"
+	"go.opencensus.io/stats/view"
+	"go.opencensus.io/tag"
+)
+
+func TestClientViews(t *testing.T) {
+	for _, v := range []*view.View{
+		ClientSentBytesDistribution,
+		ClientReceivedBytesDistribution,
+		ClientRoundtripLatencyDistribution,
+		ClientCompletedCount,
+	} {
+
+		if v.Measure == nil {
+			t.Fatalf("nil measure: %v", v)
+		}
+		if m := v.Measure.Name(); !strings.HasPrefix(m, "opencensus.io/http/client/") {
+			t.Errorf("Unexpected measure name prefix: %v", v)
+		}
+		if v.Name == "" {
+			t.Errorf("Empty name: %v", v)
+		}
+		if !strings.HasPrefix(v.Name, "opencensus.io/http/client/") {
+			t.Errorf("Unexpected prefix: %s", v.Name)
+		}
+		if v.Description == "" {
+			t.Errorf("Empty description: %s", v.Name)
+		}
+		if !reflect.DeepEqual(v.TagKeys, []tag.Key{KeyClientMethod, KeyClientStatus}) {
+			t.Errorf("Unexpected tags for client view %s: %v", v.Name, v.TagKeys)
+		}
+		if strings.HasSuffix(v.Description, ".") {
+			t.Errorf("View description should not end with a period: %s", v.Name)
+		}
+	}
+}
+
+func TestClientTagKeys(t *testing.T) {
+	for _, k := range []tag.Key{
+		KeyClientStatus,
+		KeyClientMethod,
+		KeyClientHost,
+		KeyClientPath,
+	} {
+		if !strings.HasPrefix(k.Name(), "http_client_") {
+			t.Errorf("Unexpected prefix: %s", k.Name())
+		}
+	}
+}
+
+func TestClientMeasures(t *testing.T) {
+	for _, m := range []stats.Measure{
+		ClientSentBytes,
+		ClientReceivedBytes,
+		ClientRoundtripLatency,
+	} {
+		if !strings.HasPrefix(m.Name(), "opencensus.io/http/client/") {
+			t.Errorf("Unexpected prefix: %v", m)
+		}
+		if strings.HasSuffix(m.Description(), ".") {
+			t.Errorf("View description should not end with a period: %s", m.Name())
+		}
+		if len(m.Unit()) == 0 {
+			t.Errorf("No unit: %s", m.Name())
+		}
+	}
+}
diff --git a/plugin/ochttp/trace_test.go b/plugin/ochttp/trace_test.go
index eb1728f..ea9b77c 100644
--- a/plugin/ochttp/trace_test.go
+++ b/plugin/ochttp/trace_test.go
@@ -79,6 +79,26 @@ func (t testPropagator) SpanContextToRequest(sc trace.SpanContext, req *http.Req
 	req.Header.Set("trace", hex.EncodeToString(buf.Bytes()))
 }
 
+func TestTransport_RoundTrip_Race(t *testing.T) {
+	// This tests that we don't modify the request in accordance with the
+	// specification for http.RoundTripper.
+	// We attempt to trigger a race by reading the request from a separate
+	// goroutine. If the request is modified by Transport, this should trigger
+	// the race detector.
+
+	transport := &testTransport{ch: make(chan *http.Request, 1)}
+	rt := &Transport{
+		Propagation: &testPropagator{},
+		Base:        transport,
+	}
+	req, _ := http.NewRequest("GET", "http://foo.com", nil)
+	go func() {
+		fmt.Println(*req)
+	}()
+	rt.RoundTrip(req)
+	_ = <-transport.ch
+}
+
 func TestTransport_RoundTrip(t *testing.T) {
 	ctx := context.Background()
 	ctx, parent := trace.StartSpan(ctx, "parent")
@@ -180,8 +200,6 @@ func (c *collector) ExportSpan(s *trace.SpanData) {
 }
 
 func TestEndToEnd(t *testing.T) {
-	trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
-
 	tc := []struct {
 		name            string
 		handler         *Handler
@@ -226,12 +244,10 @@ func TestEndToEnd(t *testing.T) {
 			// Start the server.
 			serverDone := make(chan struct{})
 			serverReturn := make(chan time.Time)
+			tt.handler.StartOptions.Sampler = trace.AlwaysSample()
 			url := serveHTTP(tt.handler, serverDone, serverReturn)
 
-			// Start a root Span in the client.
-			ctx, root := trace.StartSpan(
-				context.Background(),
-				"top-level")
+			ctx := context.Background()
 			// Make the request.
 			req, err := http.NewRequest(
 				http.MethodPost,
@@ -241,7 +257,11 @@ func TestEndToEnd(t *testing.T) {
 				t.Fatal(err)
 			}
 			req = req.WithContext(ctx)
-			resp, err := tt.transport.RoundTrip(req)
+			tt.transport.StartOptions.Sampler = trace.AlwaysSample()
+			c := &http.Client{
+				Transport: tt.transport,
+			}
+			resp, err := c.Do(req)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -308,9 +328,6 @@ func TestEndToEnd(t *testing.T) {
 					t.Errorf("len(server.Links) = %d; want %d", got, want)
 				} else {
 					link := server.Links[0]
-					if got, want := link.TraceID, root.SpanContext().TraceID; got != want {
-						t.Errorf("link.TraceID = %q; want %q", got, want)
-					}
 					if got, want := link.Type, trace.LinkTypeChild; got != want {
 						t.Errorf("link.Type = %v; want %v", got, want)
 					}
@@ -391,7 +408,12 @@ func TestFormatSpanName(t *testing.T) {
 	defer server.Close()
 
 	client := &http.Client{
-		Transport: &Transport{FormatSpanName: formatSpanName},
+		Transport: &Transport{
+			FormatSpanName: formatSpanName,
+			StartOptions: trace.StartOptions{
+				Sampler: trace.AlwaysSample(),
+			},
+		},
 	}
 
 	tests := []struct {
@@ -500,16 +522,16 @@ func TestStatusUnitTest(t *testing.T) {
 		in   int
 		want trace.Status
 	}{
-		{200, trace.Status{Code: trace.StatusCodeOK, Message: `"OK"`}},
-		{204, trace.Status{Code: trace.StatusCodeOK, Message: `"OK"`}},
-		{100, trace.Status{Code: trace.StatusCodeUnknown, Message: `"UNKNOWN"`}},
-		{500, trace.Status{Code: trace.StatusCodeUnknown, Message: `"UNKNOWN"`}},
-		{404, trace.Status{Code: trace.StatusCodeNotFound, Message: `"NOT_FOUND"`}},
-		{600, trace.Status{Code: trace.StatusCodeUnknown, Message: `"UNKNOWN"`}},
-		{401, trace.Status{Code: trace.StatusCodeUnauthenticated, Message: `"UNAUTHENTICATED"`}},
-		{403, trace.Status{Code: trace.StatusCodePermissionDenied, Message: `"PERMISSION_DENIED"`}},
-		{301, trace.Status{Code: trace.StatusCodeOK, Message: `"OK"`}},
-		{501, trace.Status{Code: trace.StatusCodeUnimplemented, Message: `"UNIMPLEMENTED"`}},
+		{200, trace.Status{Code: trace.StatusCodeOK, Message: `OK`}},
+		{204, trace.Status{Code: trace.StatusCodeOK, Message: `OK`}},
+		{100, trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}},
+		{500, trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}},
+		{404, trace.Status{Code: trace.StatusCodeNotFound, Message: `NOT_FOUND`}},
+		{600, trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}},
+		{401, trace.Status{Code: trace.StatusCodeUnauthenticated, Message: `UNAUTHENTICATED`}},
+		{403, trace.Status{Code: trace.StatusCodePermissionDenied, Message: `PERMISSION_DENIED`}},
+		{301, trace.Status{Code: trace.StatusCodeOK, Message: `OK`}},
+		{501, trace.Status{Code: trace.StatusCodeUnimplemented, Message: `UNIMPLEMENTED`}},
 	}
 
 	for _, tt := range tests {
diff --git a/resource/resource_test.go b/resource/resource_test.go
new file mode 100644
index 0000000..77fef04
--- /dev/null
+++ b/resource/resource_test.go
@@ -0,0 +1,163 @@
+// 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 resource
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestMerge(t *testing.T) {
+	cases := []struct {
+		a, b, want *Resource
+	}{
+		{
+			a: &Resource{
+				Type:   "t1",
+				Labels: map[string]string{"a": "1", "b": "2"},
+			},
+			b: &Resource{
+				Type:   "t2",
+				Labels: map[string]string{"a": "1", "b": "3", "c": "4"},
+			},
+			want: &Resource{
+				Type:   "t1",
+				Labels: map[string]string{"a": "1", "b": "2", "c": "4"},
+			},
+		},
+		{
+			a: nil,
+			b: &Resource{
+				Type:   "t1",
+				Labels: map[string]string{"a": "1"},
+			},
+			want: &Resource{
+				Type:   "t1",
+				Labels: map[string]string{"a": "1"},
+			},
+		},
+		{
+			a: &Resource{
+				Type:   "t1",
+				Labels: map[string]string{"a": "1"},
+			},
+			b: nil,
+			want: &Resource{
+				Type:   "t1",
+				Labels: map[string]string{"a": "1"},
+			},
+		},
+	}
+	for i, c := range cases {
+		t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
+			res := merge(c.a, c.b)
+			if !reflect.DeepEqual(res, c.want) {
+				t.Fatalf("unwanted result: want %+v, got %+v", c.want, res)
+			}
+		})
+	}
+}
+
+func TestDecodeLabels(t *testing.T) {
+	cases := []struct {
+		encoded    string
+		wantLabels map[string]string
+		wantFail   bool
+	}{
+		{
+			encoded:    `example.org/test-1="test $ \"" ,  Abc="Def"`,
+			wantLabels: map[string]string{"example.org/test-1": "test $ \"", "Abc": "Def"},
+		}, {
+			encoded:    `single="key"`,
+			wantLabels: map[string]string{"single": "key"},
+		},
+		{encoded: `invalid-char-ü="test"`, wantFail: true},
+		{encoded: `invalid-char="ü-test"`, wantFail: true},
+		{encoded: `missing="trailing-quote`, wantFail: true},
+		{encoded: `missing=leading-quote"`, wantFail: true},
+		{encoded: `extra="chars", a`, wantFail: true},
+	}
+	for i, c := range cases {
+		t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) {
+			res, err := DecodeLabels(c.encoded)
+			if err != nil && !c.wantFail {
+				t.Fatalf("unwanted error: %s", err)
+			}
+			if c.wantFail && err == nil {
+				t.Fatalf("wanted failure but got none, result: %v", res)
+			}
+			if !reflect.DeepEqual(res, c.wantLabels) {
+				t.Fatalf("wanted result %v, got %v", c.wantLabels, res)
+			}
+		})
+	}
+}
+
+func TestEncodeLabels(t *testing.T) {
+	got := EncodeLabels(map[string]string{
+		"example.org/test-1": "test ¥ \"",
+		"un":                 "quøted",
+		"Abc":                "Def",
+	})
+	if want := `Abc="Def",example.org/test-1="test ¥ \"",un="quøted"`; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
+
+func TestMultiDetector(t *testing.T) {
+	got, err := MultiDetector(
+		func(context.Context) (*Resource, error) {
+			return &Resource{
+				Type:   "t1",
+				Labels: map[string]string{"a": "1", "b": "2"},
+			}, nil
+		},
+		func(context.Context) (*Resource, error) {
+			return &Resource{
+				Type:   "t2",
+				Labels: map[string]string{"a": "11", "c": "3"},
+			}, nil
+		},
+	)(context.Background())
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+	want := &Resource{
+		Type:   "t1",
+		Labels: map[string]string{"a": "1", "b": "2", "c": "3"},
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Fatalf("unexpected resource: want %v, got %v", want, got)
+	}
+
+	wantErr := errors.New("err1")
+	got, err = MultiDetector(
+		func(context.Context) (*Resource, error) {
+			return &Resource{
+				Type:   "t1",
+				Labels: map[string]string{"a": "1", "b": "2"},
+			}, nil
+		},
+		func(context.Context) (*Resource, error) {
+			return nil, wantErr
+		},
+	)(context.Background())
+	if err != wantErr {
+		t.Fatalf("unexpected error: want %v, got %v", wantErr, err)
+	}
+}
diff --git a/stats/view/aggregation_data_test.go b/stats/view/aggregation_data_test.go
index 359ffa6..9b12b85 100644
--- a/stats/view/aggregation_data_test.go
+++ b/stats/view/aggregation_data_test.go
@@ -18,6 +18,11 @@ package view
 import (
 	"reflect"
 	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"go.opencensus.io/exemplar"
 )
 
 func TestDataClone(t *testing.T) {
@@ -59,3 +64,82 @@ func TestDataClone(t *testing.T) {
 		})
 	}
 }
+
+func TestDistributionData_addSample(t *testing.T) {
+	dd := newDistributionData([]float64{0, 1, 2})
+	t1, _ := time.Parse("Mon Jan 2 15:04:05 -0700 MST 2006", "Mon Jan 2 15:04:05 -0700 MST 2006")
+	e1 := &exemplar.Exemplar{
+		Attachments: exemplar.Attachments{
+			"tag:X": "Y",
+			"tag:A": "B",
+		},
+		Timestamp: t1,
+		Value:     0.5,
+	}
+	dd.addSample(e1)
+
+	want := &DistributionData{
+		Count:              1,
+		CountPerBucket:     []int64{0, 1, 0, 0},
+		ExemplarsPerBucket: []*exemplar.Exemplar{nil, e1, nil, nil},
+		Max:                0.5,
+		Min:                0.5,
+		Mean:               0.5,
+		SumOfSquaredDev:    0,
+	}
+	if diff := cmpDD(dd, want); diff != "" {
+		t.Fatalf("Unexpected DistributionData -got +want: %s", diff)
+	}
+
+	t2 := t1.Add(time.Microsecond)
+	e2 := &exemplar.Exemplar{
+		Attachments: exemplar.Attachments{
+			"tag:X": "Y",
+		},
+		Timestamp: t2,
+		Value:     0.7,
+	}
+	dd.addSample(e2)
+
+	// Previous exemplar should be preserved, since it has more annotations.
+	want = &DistributionData{
+		Count:              2,
+		CountPerBucket:     []int64{0, 2, 0, 0},
+		ExemplarsPerBucket: []*exemplar.Exemplar{nil, e1, nil, nil},
+		Max:                0.7,
+		Min:                0.5,
+		Mean:               0.6,
+		SumOfSquaredDev:    0,
+	}
+	if diff := cmpDD(dd, want); diff != "" {
+		t.Fatalf("Unexpected DistributionData -got +want: %s", diff)
+	}
+
+	t3 := t2.Add(time.Microsecond)
+	e3 := &exemplar.Exemplar{
+		Attachments: exemplar.Attachments{
+			exemplar.KeyTraceID: "abcd",
+		},
+		Timestamp: t3,
+		Value:     0.2,
+	}
+	dd.addSample(e3)
+
+	// Exemplar should be replaced since it has a trace_id.
+	want = &DistributionData{
+		Count:              3,
+		CountPerBucket:     []int64{0, 3, 0, 0},
+		ExemplarsPerBucket: []*exemplar.Exemplar{nil, e3, nil, nil},
+		Max:                0.7,
+		Min:                0.2,
+		Mean:               0.4666666666666667,
+		SumOfSquaredDev:    0,
+	}
+	if diff := cmpDD(dd, want); diff != "" {
+		t.Fatalf("Unexpected DistributionData -got +want: %s", diff)
+	}
+}
+
+func cmpDD(got, want *DistributionData) string {
+	return cmp.Diff(got, want, cmpopts.IgnoreFields(DistributionData{}, "SumOfSquaredDev"), cmpopts.IgnoreUnexported(DistributionData{}))
+}
diff --git a/stats/view/benchmark_test.go b/stats/view/benchmark_test.go
index f5d0efa..0f195d4 100644
--- a/stats/view/benchmark_test.go
+++ b/stats/view/benchmark_test.go
@@ -19,6 +19,7 @@ import (
 	"context"
 	"fmt"
 	"testing"
+	"time"
 
 	"go.opencensus.io/stats"
 	"go.opencensus.io/tag"
@@ -84,6 +85,7 @@ func BenchmarkRecordReqCommand(b *testing.B) {
 				m.M(1),
 			},
 			tm: tag.FromContext(ctxs[i%len(ctxs)]),
+			t:  time.Now(),
 		}
 		record.handleCommand(w)
 	}
diff --git a/stats/view/view_test.go b/stats/view/view_test.go
index 45a4bd3..445e56a 100644
--- a/stats/view/view_test.go
+++ b/stats/view/view_test.go
@@ -19,6 +19,8 @@ import (
 	"context"
 	"testing"
 
+	"go.opencensus.io/exemplar"
+
 	"go.opencensus.io/stats"
 	"go.opencensus.io/tag"
 )
@@ -65,7 +67,7 @@ func Test_View_MeasureFloat64_AggregationDistribution(t *testing.T) {
 				{
 					[]tag.Tag{{Key: k1, Value: "v1"}},
 					&DistributionData{
-						2, 1, 5, 3, 8, []int64{1, 1}, []float64{2},
+						Count: 2, Min: 1, Max: 5, Mean: 3, SumOfSquaredDev: 8, CountPerBucket: []int64{1, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 			},
@@ -80,13 +82,13 @@ func Test_View_MeasureFloat64_AggregationDistribution(t *testing.T) {
 				{
 					[]tag.Tag{{Key: k1, Value: "v1"}},
 					&DistributionData{
-						1, 1, 1, 1, 0, []int64{1, 0}, []float64{2},
+						Count: 1, Min: 1, Max: 1, Mean: 1, CountPerBucket: []int64{1, 0}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 				{
 					[]tag.Tag{{Key: k2, Value: "v2"}},
 					&DistributionData{
-						1, 5, 5, 5, 0, []int64{0, 1}, []float64{2},
+						Count: 1, Min: 5, Max: 5, Mean: 5, CountPerBucket: []int64{0, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 			},
@@ -104,25 +106,25 @@ func Test_View_MeasureFloat64_AggregationDistribution(t *testing.T) {
 				{
 					[]tag.Tag{{Key: k1, Value: "v1"}},
 					&DistributionData{
-						2, 1, 5, 3, 8, []int64{1, 1}, []float64{2},
+						Count: 2, Min: 1, Max: 5, Mean: 3, SumOfSquaredDev: 8, CountPerBucket: []int64{1, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 				{
 					[]tag.Tag{{Key: k1, Value: "v1 other"}},
 					&DistributionData{
-						1, 1, 1, 1, 0, []int64{1, 0}, []float64{2},
+						Count: 1, Min: 1, Max: 1, Mean: 1, CountPerBucket: []int64{1, 0}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 				{
 					[]tag.Tag{{Key: k2, Value: "v2"}},
 					&DistributionData{
-						1, 5, 5, 5, 0, []int64{0, 1}, []float64{2},
+						Count: 1, Min: 5, Max: 5, Mean: 5, CountPerBucket: []int64{0, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 				{
 					[]tag.Tag{{Key: k1, Value: "v1"}, {Key: k2, Value: "v2"}},
 					&DistributionData{
-						1, 5, 5, 5, 0, []int64{0, 1}, []float64{2},
+						Count: 1, Min: 5, Max: 5, Mean: 5, CountPerBucket: []int64{0, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 			},
@@ -142,19 +144,19 @@ func Test_View_MeasureFloat64_AggregationDistribution(t *testing.T) {
 				{
 					[]tag.Tag{{Key: k1, Value: "v1 is a very long value key"}},
 					&DistributionData{
-						2, 1, 5, 3, 8, []int64{1, 1}, []float64{2},
+						Count: 2, Min: 1, Max: 5, Mean: 3, SumOfSquaredDev: 8, CountPerBucket: []int64{1, 1}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 				{
 					[]tag.Tag{{Key: k1, Value: "v1 is another very long value key"}},
 					&DistributionData{
-						1, 1, 1, 1, 0, []int64{1, 0}, []float64{2},
+						Count: 1, Min: 1, Max: 1, Mean: 1, CountPerBucket: []int64{1, 0}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 				{
 					[]tag.Tag{{Key: k1, Value: "v1 is a very long value key"}, {Key: k2, Value: "v2 is a very long value key"}},
 					&DistributionData{
-						4, 1, 5, 3, 2.66666666666667 * 3, []int64{1, 3}, []float64{2},
+						Count: 4, Min: 1, Max: 5, Mean: 3, SumOfSquaredDev: 2.66666666666667 * 3, CountPerBucket: []int64{1, 3}, bounds: []float64{2}, ExemplarsPerBucket: []*exemplar.Exemplar{nil, nil},
 					},
 				},
 			},
@@ -173,7 +175,11 @@ func Test_View_MeasureFloat64_AggregationDistribution(t *testing.T) {
 			if err != nil {
 				t.Errorf("%v: New = %v", tc.label, err)
 			}
-			view.addSample(tag.FromContext(ctx), r.f)
+			e := &exemplar.Exemplar{
+				Value:       r.f,
+				Attachments: exemplar.AttachmentsFromContext(ctx),
+			}
+			view.addSample(tag.FromContext(ctx), e)
 		}
 
 		gotRows := view.collectedRows()
@@ -289,7 +295,10 @@ func Test_View_MeasureFloat64_AggregationSum(t *testing.T) {
 			if err != nil {
 				t.Errorf("%v: New = %v", tt.label, err)
 			}
-			view.addSample(tag.FromContext(ctx), r.f)
+			e := &exemplar.Exemplar{
+				Value: r.f,
+			}
+			view.addSample(tag.FromContext(ctx), e)
 		}
 
 		gotRows := view.collectedRows()
@@ -399,3 +408,39 @@ func TestRegisterUnregisterParity(t *testing.T) {
 		}
 	}
 }
+
+func TestRegisterAfterMeasurement(t *testing.T) {
+	// Tests that we can register views after measurements are created and
+	// they still take effect.
+
+	m := stats.Int64(t.Name(), "", stats.UnitDimensionless)
+	mm := m.M(1)
+	ctx := context.Background()
+
+	stats.Record(ctx, mm)
+	v := &View{
+		Measure:     m,
+		Aggregation: Count(),
+	}
+	if err := Register(v); err != nil {
+		t.Fatal(err)
+	}
+
+	rows, err := RetrieveData(v.Name)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(rows) > 0 {
+		t.Error("View should not have data")
+	}
+
+	stats.Record(ctx, mm)
+
+	rows, err = RetrieveData(v.Name)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(rows) == 0 {
+		t.Error("View should have data")
+	}
+}
diff --git a/tag/context_test.go b/tag/context_test.go
new file mode 100644
index 0000000..e85b1c4
--- /dev/null
+++ b/tag/context_test.go
@@ -0,0 +1,44 @@
+// 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 tag
+
+import (
+	"context"
+	"testing"
+)
+
+func TestExtractTagsAttachment(t *testing.T) {
+	// We can't depend on the stats of view package without creating a
+	// dependency cycle.
+
+	var m map[string]string
+	ctx := context.Background()
+
+	res := extractTagsAttachments(ctx, m)
+	if res != nil {
+		t.Fatalf("res = %v; want nil", res)
+	}
+
+	k, _ := NewKey("test")
+	ctx, _ = New(ctx, Insert(k, "test123"))
+	res = extractTagsAttachments(ctx, m)
+	if res == nil {
+		t.Fatal("res = nil")
+	}
+	if got, want := res["tag:test"], "test123"; got != want {
+		t.Fatalf("res[Tags:test] = %v; want %v", got, want)
+	}
+}
diff --git a/trace/exemplar_test.go b/trace/exemplar_test.go
new file mode 100644
index 0000000..de27631
--- /dev/null
+++ b/trace/exemplar_test.go
@@ -0,0 +1,100 @@
+// 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 trace_test
+
+import (
+	"context"
+	"testing"
+
+	"go.opencensus.io/stats"
+	"go.opencensus.io/stats/view"
+	"go.opencensus.io/trace"
+)
+
+func TestTraceExemplar(t *testing.T) {
+	m := stats.Float64("measure."+t.Name(), "", stats.UnitDimensionless)
+	v := &view.View{
+		Measure:     m,
+		Aggregation: view.Distribution(0, 1, 2, 3),
+	}
+	view.Register(v)
+	ctx := context.Background()
+	ctx, span := trace.StartSpan(ctx, t.Name(), trace.WithSampler(trace.AlwaysSample()))
+	stats.Record(ctx, m.M(1.5))
+	span.End()
+
+	rows, err := view.RetrieveData(v.Name)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(rows) == 0 {
+		t.Fatal("len(rows) = 0; want > 0")
+	}
+	dd := rows[0].Data.(*view.DistributionData)
+	if got := len(dd.ExemplarsPerBucket); got < 3 {
+		t.Fatalf("len(dd.ExemplarsPerBucket) = %d; want >= 2", got)
+	}
+	exemplar := dd.ExemplarsPerBucket[2]
+	if exemplar == nil {
+		t.Fatal("Expected exemplar")
+	}
+	if got, want := exemplar.Value, 1.5; got != want {
+		t.Fatalf("exemplar.Value = %v; got %v", got, want)
+	}
+	if _, ok := exemplar.Attachments["trace_id"]; !ok {
+		t.Fatalf("exemplar.Attachments = %v; want trace_id key", exemplar.Attachments)
+	}
+	if _, ok := exemplar.Attachments["span_id"]; !ok {
+		t.Fatalf("exemplar.Attachments = %v; want span_id key", exemplar.Attachments)
+	}
+}
+
+func TestTraceExemplar_notSampled(t *testing.T) {
+	m := stats.Float64("measure."+t.Name(), "", stats.UnitDimensionless)
+	v := &view.View{
+		Measure:     m,
+		Aggregation: view.Distribution(0, 1, 2, 3),
+	}
+	view.Register(v)
+	ctx := context.Background()
+	ctx, span := trace.StartSpan(ctx, t.Name(), trace.WithSampler(trace.NeverSample()))
+	stats.Record(ctx, m.M(1.5))
+	span.End()
+
+	rows, err := view.RetrieveData(v.Name)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(rows) == 0 {
+		t.Fatal("len(rows) = 0; want > 0")
+	}
+	dd := rows[0].Data.(*view.DistributionData)
+	if got := len(dd.ExemplarsPerBucket); got < 3 {
+		t.Fatalf("len(buckets) = %d; want >= 2", got)
+	}
+	exemplar := dd.ExemplarsPerBucket[2]
+	if exemplar == nil {
+		t.Fatal("Expected exemplar")
+	}
+	if got, want := exemplar.Value, 1.5; got != want {
+		t.Fatalf("exemplar.Value = %v; got %v", got, want)
+	}
+	if _, ok := exemplar.Attachments["trace_id"]; ok {
+		t.Fatalf("exemplar.Attachments = %v; want no trace_id", exemplar.Attachments)
+	}
+	if _, ok := exemplar.Attachments["span_id"]; ok {
+		t.Fatalf("exemplar.Attachments = %v; want span_id key", exemplar.Attachments)
+	}
+}
diff --git a/trace/trace_test.go b/trace/trace_test.go
index 234531b..0c25cc7 100644
--- a/trace/trace_test.go
+++ b/trace/trace_test.go
@@ -18,13 +18,17 @@ import (
 	"context"
 	"fmt"
 	"reflect"
+	"sync/atomic"
 	"testing"
 	"time"
+
+	"go.opencensus.io/trace/tracestate"
 )
 
 var (
-	tid = TraceID{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 4, 8, 16, 32, 64, 128}
-	sid = SpanID{1, 2, 4, 8, 16, 32, 64, 128}
+	tid               = TraceID{1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 4, 8, 16, 32, 64, 128}
+	sid               = SpanID{1, 2, 4, 8, 16, 32, 64, 128}
+	testTracestate, _ = tracestate.New(nil, tracestate.Entry{Key: "foo", Value: "bar"})
 )
 
 func init() {
@@ -70,6 +74,9 @@ func checkChild(p SpanContext, c *Span) error {
 	if got, want := c.spanContext.TraceOptions, p.TraceOptions; got != want {
 		return fmt.Errorf("got child trace options %d, want %d", got, want)
 	}
+	if got, want := c.spanContext.Tracestate, p.Tracestate; got != want {
+		return fmt.Errorf("got child tracestate %v, want %v", got, want)
+	}
 	return nil
 }
 
@@ -204,6 +211,7 @@ func TestStartSpanWithRemoteParent(t *testing.T) {
 		TraceID:      tid,
 		SpanID:       sid,
 		TraceOptions: 0x1,
+		Tracestate:   testTracestate,
 	}
 	ctx, _ = StartSpanWithRemoteParent(context.Background(), "startSpanWithRemoteParent", sc)
 	if err := checkChild(sc, FromContext(ctx)); err != nil {
@@ -668,3 +676,39 @@ func TestStartSpanAfterEnd(t *testing.T) {
 		t.Errorf("span-2.ParentSpanID=%q; want %q (span1.SpanID)", got, want)
 	}
 }
+
+func TestNilSpanEnd(t *testing.T) {
+	var span *Span
+	span.End()
+}
+
+func TestExecutionTracerTaskEnd(t *testing.T) {
+	var n uint64
+	executionTracerTaskEnd := func() {
+		atomic.AddUint64(&n, 1)
+	}
+
+	var spans []*Span
+	_, span := StartSpan(context.Background(), "foo", WithSampler(NeverSample()))
+	span.executionTracerTaskEnd = executionTracerTaskEnd
+	spans = append(spans, span) // never sample
+
+	_, span = StartSpanWithRemoteParent(context.Background(), "foo", SpanContext{
+		TraceID:      TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
+		SpanID:       SpanID{0, 1, 2, 3, 4, 5, 6, 7},
+		TraceOptions: 0,
+	})
+	span.executionTracerTaskEnd = executionTracerTaskEnd
+	spans = append(spans, span) // parent not sampled
+
+	_, span = StartSpan(context.Background(), "foo", WithSampler(AlwaysSample()))
+	span.executionTracerTaskEnd = executionTracerTaskEnd
+	spans = append(spans, span) // always sample
+
+	for _, span := range spans {
+		span.End()
+	}
+	if got, want := n, uint64(len(spans)); got != want {
+		t.Fatalf("Execution tracer task ended for %v spans; want %v", got, want)
+	}
+}
diff --git a/trace/tracestate/tracestate_test.go b/trace/tracestate/tracestate_test.go
new file mode 100644
index 0000000..db76d9c
--- /dev/null
+++ b/trace/tracestate/tracestate_test.go
@@ -0,0 +1,313 @@
+// 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 tracestate
+
+import (
+	"fmt"
+	"testing"
+)
+
+func checkFront(t *testing.T, tracestate *Tracestate, wantKey, testname string) {
+	gotKey := tracestate.entries[0].Key
+	if gotKey != wantKey {
+		t.Errorf("test:%s: first entry in the list: got %q want %q", testname, gotKey, wantKey)
+	}
+}
+
+func checkBack(t *testing.T, tracestate *Tracestate, wantKey, testname string) {
+	gotKey := tracestate.entries[len(tracestate.entries)-1].Key
+	if gotKey != wantKey {
+		t.Errorf("test:%s: last entry in the list: got %q want %q", testname, gotKey, wantKey)
+	}
+}
+
+func checkSize(t *testing.T, tracestate *Tracestate, wantSize int, testname string) {
+	if gotSize := len(tracestate.entries); gotSize != wantSize {
+		t.Errorf("test:%s: size of the list: got %q want %q", testname, gotSize, wantSize)
+	}
+}
+
+func (ts *Tracestate) get(key string) (string, bool) {
+	if ts == nil {
+		return "", false
+	}
+	for _, entry := range ts.entries {
+		if entry.Key == key {
+			return entry.Value, true
+		}
+	}
+	return "", false
+}
+
+func checkKeyValue(t *testing.T, tracestate *Tracestate, key, wantValue, testname string) {
+	wantOk := true
+	if wantValue == "" {
+		wantOk = false
+	}
+	gotValue, gotOk := tracestate.get(key)
+	if wantOk != gotOk || gotValue != wantValue {
+		t.Errorf("test:%s: get value for key=%s failed: got %q want %q", testname, key, gotValue, wantValue)
+	}
+}
+
+func checkError(t *testing.T, tracestate *Tracestate, err error, testname, msg string) {
+	if err != nil {
+		t.Errorf("test:%s: %s: tracestate=%v, error= %v", testname, msg, tracestate, err)
+	}
+}
+
+func wantError(t *testing.T, tracestate *Tracestate, err error, testname, msg string) {
+	if err == nil {
+		t.Errorf("test:%s: %s: tracestate=%v, error=%v", testname, msg, tracestate, err)
+	}
+}
+
+func TestCreateWithNullParent(t *testing.T) {
+	key1, value1 := "hello", "world"
+	testname := "TestCreateWithNullParent"
+
+	entry := Entry{key1, value1}
+	tracestate, err := New(nil, entry)
+	checkError(t, tracestate, err, testname, "create failed from null parent")
+	checkKeyValue(t, tracestate, key1, value1, testname)
+}
+
+func TestCreateFromParentWithSingleKey(t *testing.T) {
+	key1, value1, key2, value2 := "hello", "world", "foo", "bar"
+	testname := "TestCreateFromParentWithSingleKey"
+
+	entry1 := Entry{key1, value1}
+	entry2 := Entry{key2, value2}
+	parent, _ := New(nil, entry1)
+	tracestate, err := New(parent, entry2)
+
+	checkError(t, tracestate, err, testname, "create failed from parent with single key")
+	checkKeyValue(t, tracestate, key2, value2, testname)
+	checkFront(t, tracestate, key2, testname)
+	checkBack(t, tracestate, key1, testname)
+}
+
+func TestCreateFromParentWithDoubleKeys(t *testing.T) {
+	key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "bar", "baz"
+	testname := "TestCreateFromParentWithDoubleKeys"
+
+	entry1 := Entry{key1, value1}
+	entry2 := Entry{key2, value2}
+	entry3 := Entry{key3, value3}
+	parent, _ := New(nil, entry2, entry1)
+	tracestate, err := New(parent, entry3)
+
+	checkError(t, tracestate, err, testname, "create failed from parent with double keys")
+	checkKeyValue(t, tracestate, key3, value3, testname)
+	checkFront(t, tracestate, key3, testname)
+	checkBack(t, tracestate, key1, testname)
+}
+
+func TestCreateFromParentWithExistingKey(t *testing.T) {
+	key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "hello", "baz"
+	testname := "TestCreateFromParentWithExistingKey"
+
+	entry1 := Entry{key1, value1}
+	entry2 := Entry{key2, value2}
+	entry3 := Entry{key3, value3}
+	parent, _ := New(nil, entry2, entry1)
+	tracestate, err := New(parent, entry3)
+
+	checkError(t, tracestate, err, testname, "create failed with an existing key")
+	checkKeyValue(t, tracestate, key3, value3, testname)
+	checkFront(t, tracestate, key3, testname)
+	checkBack(t, tracestate, key2, testname)
+	checkSize(t, tracestate, 2, testname)
+}
+
+func TestImplicitImmutableTracestate(t *testing.T) {
+	key1, value1, key2, value2, key3, value3 := "hello", "world", "hello", "bar", "foo", "baz"
+	testname := "TestImplicitImmutableTracestate"
+
+	entry1 := Entry{key1, value1}
+	entry2 := Entry{key2, value2}
+	parent, _ := New(nil, entry1)
+	tracestate, err := New(parent, entry2)
+
+	checkError(t, tracestate, err, testname, "create failed")
+	checkKeyValue(t, tracestate, key2, value2, testname)
+	checkKeyValue(t, parent, key2, value1, testname)
+
+	// Get and update entries.
+	entries := tracestate.Entries()
+	entry := Entry{key3, value3}
+	entries = append(entries, entry)
+
+	// Check Tracestate does not have key3.
+	checkKeyValue(t, tracestate, key3, "", testname)
+}
+
+func TestKeyWithValidChar(t *testing.T) {
+	testname := "TestKeyWithValidChar"
+
+	arrayRune := []rune("")
+	for c := 'a'; c <= 'z'; c++ {
+		arrayRune = append(arrayRune, c)
+	}
+	for c := '0'; c <= '9'; c++ {
+		arrayRune = append(arrayRune, c)
+	}
+	arrayRune = append(arrayRune, '_')
+	arrayRune = append(arrayRune, '-')
+	arrayRune = append(arrayRune, '*')
+	arrayRune = append(arrayRune, '/')
+	key := string(arrayRune)
+	entry := Entry{key, "world"}
+	tracestate, err := New(nil, entry)
+
+	checkError(t, tracestate, err, testname, "create failed when the key contains all valid characters")
+}
+
+func TestKeyWithInvalidChar(t *testing.T) {
+	testname := "TestKeyWithInvalidChar"
+
+	keys := []string{"1ab", "1ab2", "Abc", " abc", "a=b"}
+
+	for _, key := range keys {
+		entry := Entry{key, "world"}
+		tracestate, err := New(nil, entry)
+		wantError(t, tracestate, err, testname, fmt.Sprintf(
+			"create did not err with invalid key=%q", key))
+	}
+}
+
+func TestNilKey(t *testing.T) {
+	testname := "TestNilKey"
+
+	entry := Entry{"", "world"}
+	tracestate, err := New(nil, entry)
+	wantError(t, tracestate, err, testname, "create did not err when the key is nil (\"\")")
+}
+
+func TestValueWithInvalidChar(t *testing.T) {
+	testname := "TestValueWithInvalidChar"
+
+	keys := []string{"A=B", "A,B", "AB "}
+
+	for _, value := range keys {
+		entry := Entry{"hello", value}
+		tracestate, err := New(nil, entry)
+		wantError(t, tracestate, err, testname,
+			fmt.Sprintf("create did not err when the value is invalid (%q)", value))
+	}
+}
+
+func TestNilValue(t *testing.T) {
+	testname := "TestNilValue"
+
+	tracestate, err := New(nil, Entry{"hello", ""})
+	wantError(t, tracestate, err, testname, "create did not err when the value is nil (\"\")")
+}
+
+func TestInvalidKeyLen(t *testing.T) {
+	testname := "TestInvalidKeyLen"
+
+	arrayRune := []rune("")
+	for i := 0; i <= keyMaxSize+1; i++ {
+		arrayRune = append(arrayRune, 'a')
+	}
+	key := string(arrayRune)
+	tracestate, err := New(nil, Entry{key, "world"})
+
+	wantError(t, tracestate, err, testname,
+		fmt.Sprintf("create did not err when the length (%d) of the key is larger than max (%d)",
+			len(key), keyMaxSize))
+}
+
+func TestInvalidValueLen(t *testing.T) {
+	testname := "TestInvalidValueLen"
+
+	arrayRune := []rune("")
+	for i := 0; i <= valueMaxSize+1; i++ {
+		arrayRune = append(arrayRune, 'a')
+	}
+	value := string(arrayRune)
+	tracestate, err := New(nil, Entry{"hello", value})
+
+	wantError(t, tracestate, err, testname,
+		fmt.Sprintf("create did not err when the length (%d) of the value is larger than max (%d)",
+			len(value), valueMaxSize))
+}
+
+func TestCreateFromArrayWithOverLimitKVPairs(t *testing.T) {
+	testname := "TestCreateFromArrayWithOverLimitKVPairs"
+
+	entries := []Entry{}
+	for i := 0; i <= maxKeyValuePairs; i++ {
+		key := fmt.Sprintf("a%db", i)
+		entry := Entry{key, "world"}
+		entries = append(entries, entry)
+	}
+	tracestate, err := New(nil, entries...)
+	wantError(t, tracestate, err, testname,
+		fmt.Sprintf("create did not err when the number (%d) of key-value pairs is larger than max (%d)",
+			len(entries), maxKeyValuePairs))
+}
+
+func TestCreateFromEmptyArray(t *testing.T) {
+	testname := "TestCreateFromEmptyArray"
+
+	tracestate, err := New(nil, nil...)
+	checkError(t, tracestate, err, testname,
+		"failed to create nil tracestate")
+}
+
+func TestCreateFromParentWithOverLimitKVPairs(t *testing.T) {
+	testname := "TestCreateFromParentWithOverLimitKVPairs"
+
+	entries := []Entry{}
+	for i := 0; i < maxKeyValuePairs; i++ {
+		key := fmt.Sprintf("a%db", i)
+		entry := Entry{key, "world"}
+		entries = append(entries, entry)
+	}
+	parent, err := New(nil, entries...)
+
+	checkError(t, parent, err, testname, fmt.Sprintf("create failed to add %d key-value pair", maxKeyValuePairs))
+
+	// Add one more to go over the limit
+	key := fmt.Sprintf("a%d", maxKeyValuePairs)
+	tracestate, err := New(parent, Entry{key, "world"})
+	wantError(t, tracestate, err, testname,
+		fmt.Sprintf("create did not err when attempted to exceed the key-value pair limit of %d", maxKeyValuePairs))
+}
+
+func TestCreateFromArrayWithDuplicateKeys(t *testing.T) {
+	key1, value1, key2, value2, key3, value3 := "hello", "world", "foo", "bar", "hello", "baz"
+	testname := "TestCreateFromArrayWithDuplicateKeys"
+
+	entry1 := Entry{key1, value1}
+	entry2 := Entry{key2, value2}
+	entry3 := Entry{key3, value3}
+	tracestate, err := New(nil, entry1, entry2, entry3)
+
+	wantError(t, tracestate, err, testname,
+		"create did not err when entries contained duplicate keys")
+}
+
+func TestEntriesWithNil(t *testing.T) {
+	ts, err := New(nil)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if got, want := len(ts.Entries()), 0; got != want {
+		t.Errorf("zero value should have no entries, got %v; want %v", got, want)
+	}
+}
diff --git a/zpages/rpcz_test.go b/zpages/rpcz_test.go
index 9764f6f..5a5f13b 100644
--- a/zpages/rpcz_test.go
+++ b/zpages/rpcz_test.go
@@ -49,7 +49,7 @@ func TestRpcz(t *testing.T) {
 		t.Fatal("Expected method stats not recorded")
 	}
 
-	if got, want := snapshot.CountTotal, 1; got != want {
+	if got, want := snapshot.CountTotal, uint64(1); got != want {
 		t.Errorf("snapshot.CountTotal = %d; want %d", got, want)
 	}
 }
diff --git a/zpages/templates_test.go b/zpages/templates_test.go
new file mode 100644
index 0000000..e3adf6c
--- /dev/null
+++ b/zpages/templates_test.go
@@ -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 zpages
+
+import (
+	"bytes"
+	"html/template"
+	"testing"
+)
+
+const tmplBody = `
+        <td><b>{{.Method}}</b></td>
+        <td></td>
+        <td align="right">{{.CountMinute|count}}</td>
+        <td align="right">{{.CountHour|count}}</td>
+        <td align="right">{{.CountTotal|count}}</td><td></td>
+        <td align="right">{{.AvgLatencyMinute|ms}}</td>
+        <td align="right">{{.AvgLatencyHour|ms}}</td>
+        <td align="right">{{.AvgLatencyTotal|ms}}</td><td></td>
+        <td align="right">{{.RPCRateMinute|rate}}</td>
+        <td align="right">{{.RPCRateHour|rate}}</td>
+        <td align="right">{{.RPCRateTotal|rate}}</td><td></td>
+        <td align="right">{{.InputRateMinute|datarate}}</td>
+        <td align="right">{{.InputRateHour|datarate}}</td>
+        <td align="right">{{.InputRateTotal|datarate}}</td><td></td>
+        <td align="right">{{.OutputRateMinute|datarate}}</td>
+        <td align="right">{{.OutputRateHour|datarate}}</td>
+        <td align="right">{{.OutputRateTotal|datarate}}</td><td></td>
+        <td align="right">{{.ErrorsMinute|count}}</td>
+        <td align="right">{{.ErrorsHour|count}}</td>
+        <td align="right">{{.ErrorsTotal|count}}</td><td></td>
+`
+
+var tmpl = template.Must(template.New("countTest").Funcs(templateFunctions).Parse(tmplBody))
+
+func TestTemplateFuncs(t *testing.T) {
+	buf := new(bytes.Buffer)
+	sshot := &statSnapshot{
+		Method:           "Foo",
+		CountMinute:      1e9,
+		CountHour:        5000,
+		CountTotal:       1e12,
+		AvgLatencyMinute: 10000,
+		AvgLatencyHour:   1000,
+		AvgLatencyTotal:  20000,
+		RPCRateMinute:    2000,
+		RPCRateHour:      5000,
+		RPCRateTotal:     75000,
+		InputRateMinute:  75000,
+		InputRateHour:    75000,
+		InputRateTotal:   75000,
+		OutputRateMinute: 75000,
+		OutputRateHour:   75000,
+		OutputRateTotal:  75000,
+		ErrorsMinute:     120000000,
+		ErrorsHour:       75000000,
+		ErrorsTotal:      7500000,
+	}
+	if err := tmpl.Execute(buf, sshot); err != nil {
+		t.Fatalf("Failed to execute template: %v", err)
+	}
+	want := `
+        <td><b>Foo</b></td>
+        <td></td>
+        <td align="right">1.000 G </td>
+        <td align="right">5000</td>
+        <td align="right">1.000 T </td><td></td>
+        <td align="right">0.010</td>
+        <td align="right">0.001</td>
+        <td align="right">0.020</td><td></td>
+        <td align="right">2000.000</td>
+        <td align="right">5000.000</td>
+        <td align="right">75000.000</td><td></td>
+        <td align="right">0.075</td>
+        <td align="right">0.075</td>
+        <td align="right">0.075</td><td></td>
+        <td align="right">0.075</td>
+        <td align="right">0.075</td>
+        <td align="right">0.075</td><td></td>
+        <td align="right">120.000 M </td>
+        <td align="right">75.000 M </td>
+        <td align="right">7.500 M </td><td></td>
+`
+	if g, w := buf.String(), want; g != w {
+		t.Errorf("Output mismatch:\nGot:\n\t%s\nWant:\n\t%s", g, w)
+	}
+}