exporter/zipkin: option to set RemoteEndpoint

Provide an option `WithRemoteEndpoint` that allows
setting the remote endpoint of a Zipkin exporter.
This change is necessary because the constructor
NewExporter only takes in a local endpoint, implying
that remoteEndpoint is optional but when necessary,
we need to send this information along since it is
used by Zipkin to construct a service graph.
I found this issue while working on the OpenCensus service/agent.

Fixes #959
diff --git a/exporter/zipkin/options.go b/exporter/zipkin/options.go
new file mode 100644
index 0000000..6ad98c5
--- /dev/null
+++ b/exporter/zipkin/options.go
@@ -0,0 +1,36 @@
+// Copyright 2018, OpenCensus Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zipkin
+
+import "github.com/openzipkin/zipkin-go/model"
+
+type Option interface {
+	mutateExporter(*Exporter)
+}
+
+type remoteEndpoint struct {
+	endpoint *model.Endpoint
+}
+
+var _ Option = (*remoteEndpoint)(nil)
+
+// WithRemoteEndpoint sets the remote endpoint of the exporter.
+func WithRemoteEndpoint(endpoint *model.Endpoint) Option {
+	return &remoteEndpoint{endpoint: endpoint}
+}
+
+func (re *remoteEndpoint) mutateExporter(exp *Exporter) {
+	exp.remoteEndpoint = re.endpoint
+}
diff --git a/exporter/zipkin/zipkin.go b/exporter/zipkin/zipkin.go
index 30d2fa4..4666dec 100644
--- a/exporter/zipkin/zipkin.go
+++ b/exporter/zipkin/zipkin.go
@@ -27,8 +27,9 @@
 // Exporter is an implementation of trace.Exporter that uploads spans to a
 // Zipkin server.
 type Exporter struct {
-	reporter      reporter.Reporter
-	localEndpoint *model.Endpoint
+	reporter       reporter.Reporter
+	localEndpoint  *model.Endpoint
+	remoteEndpoint *model.Endpoint
 }
 
 // NewExporter returns an implementation of trace.Exporter that uploads spans
@@ -42,16 +43,22 @@
 // constructed with github.com/openzipkin/zipkin-go.NewEndpoint, e.g.:
 // 	localEndpoint, err := NewEndpoint("my server", listener.Addr().String())
 // localEndpoint can be nil.
-func NewExporter(reporter reporter.Reporter, localEndpoint *model.Endpoint) *Exporter {
-	return &Exporter{
+func NewExporter(reporter reporter.Reporter, localEndpoint *model.Endpoint, opts ...Option) *Exporter {
+	exp := &Exporter{
 		reporter:      reporter,
 		localEndpoint: localEndpoint,
 	}
+
+	for _, opt := range opts {
+		opt.mutateExporter(exp)
+	}
+
+	return exp
 }
 
 // ExportSpan exports a span to a Zipkin server.
 func (e *Exporter) ExportSpan(s *trace.SpanData) {
-	e.reporter.Send(zipkinSpan(s, e.localEndpoint))
+	e.reporter.Send(zipkinSpan(s, e.localEndpoint, e.remoteEndpoint))
 }
 
 const (
@@ -110,7 +117,7 @@
 	return model.Undetermined
 }
 
-func zipkinSpan(s *trace.SpanData, localEndpoint *model.Endpoint) model.SpanModel {
+func zipkinSpan(s *trace.SpanData, localEndpoint, remoteEndpoint *model.Endpoint) model.SpanModel {
 	sc := s.SpanContext
 	z := model.SpanModel{
 		SpanContext: model.SpanContext{
@@ -118,11 +125,12 @@
 			ID:      convertSpanID(sc.SpanID),
 			Sampled: &sampledTrue,
 		},
-		Kind:          spanKind(s),
-		Name:          s.Name,
-		Timestamp:     s.StartTime,
-		Shared:        false,
-		LocalEndpoint: localEndpoint,
+		Kind:           spanKind(s),
+		Name:           s.Name,
+		Timestamp:      s.StartTime,
+		Shared:         false,
+		LocalEndpoint:  localEndpoint,
+		RemoteEndpoint: remoteEndpoint,
 	}
 
 	if s.ParentSpanID != (trace.SpanID{}) {
diff --git a/exporter/zipkin/zipkin_test.go b/exporter/zipkin/zipkin_test.go
index 2d5f81c..0e121d8 100644
--- a/exporter/zipkin/zipkin_test.go
+++ b/exporter/zipkin/zipkin_test.go
@@ -15,14 +15,18 @@
 package zipkin
 
 import (
+	"bytes"
 	"encoding/json"
 	"io/ioutil"
 	"net/http"
+	"net/http/httptest"
 	"reflect"
 	"strings"
+	"sync"
 	"testing"
 	"time"
 
+	openzipkin "github.com/openzipkin/zipkin-go"
 	"github.com/openzipkin/zipkin-go/model"
 	httpreporter "github.com/openzipkin/zipkin-go/reporter/http"
 	"go.opencensus.io/trace"
@@ -212,7 +216,7 @@
 		},
 	}
 	for _, tt := range tests {
-		got := zipkinSpan(tt.span, nil)
+		got := zipkinSpan(tt.span, nil, nil)
 		if len(got.Annotations) != len(tt.want.Annotations) {
 			t.Fatalf("zipkinSpan: got %d annotations in span, want %d", len(got.Annotations), len(tt.want.Annotations))
 		}
@@ -253,3 +257,84 @@
 		}
 	}
 }
+
+// Ensure that we can pass in a remote endpoint but also that it is
+// transmitted to its origina. Issue #959
+func TestRemoteEndpointOptionAndTransmission(t *testing.T) {
+	type lockableBuffer struct {
+		sync.Mutex
+		*bytes.Buffer
+	}
+
+	buf := &lockableBuffer{Mutex: sync.Mutex{}, Buffer: new(bytes.Buffer)}
+
+	cst := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		blob, _ := ioutil.ReadAll(r.Body)
+		_ = r.Body.Close()
+		buf.Lock()
+		buf.Write(blob)
+		buf.Unlock()
+	}))
+	defer cst.Close()
+
+	reporter := httpreporter.NewReporter(cst.URL, httpreporter.BatchInterval(10*time.Millisecond))
+	localEndpoint, _ := openzipkin.NewEndpoint("app", "10.0.0.17")
+	remoteEndpoint, _ := openzipkin.NewEndpoint("memcached", "10.0.0.42")
+	exp := NewExporter(reporter, localEndpoint, WithRemoteEndpoint(remoteEndpoint))
+	exp.ExportSpan(&trace.SpanData{
+		Name: "Test",
+	})
+
+	// Wait for the upload
+	<-time.After(300 * time.Millisecond)
+
+	want := `[{
+            "traceId":"0000000000000000",
+            "id":"0000000000000000",
+            "name":"Test",
+            "localEndpoint":{
+                "serviceName":"app",
+                "ipv4":"10.0.0.17"
+            },
+            "remoteEndpoint":{
+                "serviceName":"memcached","ipv4":"10.0.0.42"
+            }
+        }]`
+
+	buf.Lock()
+	got := buf.String()
+	buf.Unlock()
+
+	// Since the reported JSON could contain spaces and other indentation,
+	// strip spaces out but also the fields could be mangled so we'll instead
+	// just use an anagram equivalence to ensure all the output is present
+	replacer := strings.NewReplacer(" ", "", "\t", "", "\n", "")
+	wj := replacer.Replace(want)
+	gj := replacer.Replace(got)
+	if !anagrams(gj, wj) {
+		t.Errorf("Mismatched JSON content\nGot:\n\t%s\nWant:\n\t%s", gj, wj)
+	}
+}
+
+func anagrams(s1, s2 string) bool {
+	if len(s1) != len(s2) {
+		return false
+	}
+	if s1 == "" && s2 == "" {
+		return true
+	}
+	m1 := make(map[byte]int)
+	for i := range s1 {
+		m1[s1[i]] += 1
+		m1[s2[i]] -= 1
+	}
+
+	// Finally check that all the values are at 0
+	// that is, all the letters in s1 were matched in s2
+	for _, count := range m1 {
+		if count != 0 {
+			return false
+		}
+	}
+	return true
+}