package zipkin_test
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
zipkin "github.com/openzipkin/zipkin-go"
"github.com/openzipkin/zipkin-go/model"
"github.com/openzipkin/zipkin-go/propagation/b3"
"github.com/openzipkin/zipkin-go/reporter/recorder"
"github.com/go-kit/kit/endpoint"
zipkinkit "github.com/go-kit/kit/tracing/zipkin"
kithttp "github.com/go-kit/kit/transport/http"
)
const (
testName = "test"
testBody = "test_body"
testTagKey = "test_key"
testTagValue = "test_value"
)
func TestHTTPClientTracePropagatesParentSpan(t *testing.T) {
rec := recorder.NewReporter()
defer rec.Close()
tr, _ := zipkin.NewTracer(rec)
rURL, _ := url.Parse("https://httpbin.org/get")
clientTracer := zipkinkit.HTTPClientTrace(tr)
ep := kithttp.NewClient(
"GET",
rURL,
func(ctx context.Context, r *http.Request, i interface{}) error {
return nil
},
func(ctx context.Context, r *http.Response) (response interface{}, err error) {
return nil, nil
},
clientTracer,
).Endpoint()
parentSpan := tr.StartSpan("test")
ctx := zipkin.NewContext(context.Background(), parentSpan)
_, err := ep(ctx, nil)
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
span := spans[0]
if span.SpanContext.ParentID == nil {
t.Fatalf("incorrect parent ID, want %s have nil", parentSpan.Context().ID)
}
if want, have := parentSpan.Context().ID, *span.SpanContext.ParentID; want != have {
t.Fatalf("incorrect parent ID, want %s, have %s", want, have)
}
}
func TestHTTPClientTraceAddsExpectedTags(t *testing.T) {
dataProvider := []struct {
ResponseStatusCode int
ErrorTagValue string
}{
{http.StatusOK, ""},
{http.StatusForbidden, fmt.Sprint(http.StatusForbidden)},
}
for _, data := range dataProvider {
testHTTPClientTraceCase(t, data.ResponseStatusCode, data.ErrorTagValue)
}
}
func testHTTPClientTraceCase(t *testing.T, responseStatusCode int, errTagValue string) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(responseStatusCode)
w.Write([]byte(testBody))
}))
defer ts.Close()
rec := recorder.NewReporter()
defer rec.Close()
tr, err := zipkin.NewTracer(rec)
if err != nil {
t.Errorf("Unwanted error: %s", err.Error())
}
rMethod := "GET"
rURL, _ := url.Parse(ts.URL)
clientTracer := zipkinkit.HTTPClientTrace(
tr,
zipkinkit.Name(testName),
zipkinkit.Tags(map[string]string{testTagKey: testTagValue}),
)
ep := kithttp.NewClient(
rMethod,
rURL,
func(ctx context.Context, r *http.Request, i interface{}) error {
return nil
},
func(ctx context.Context, r *http.Response) (response interface{}, err error) {
return nil, nil
},
clientTracer,
).Endpoint()
_, err = ep(context.Background(), nil)
if err != nil {
t.Fatalf("unwanted error: %s", err.Error())
}
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, wanted %d, got %d", want, have)
}
span := spans[0]
if span.SpanContext.ParentID != nil {
t.Fatalf("incorrect parentID, wanted nil, got %s", span.SpanContext.ParentID)
}
if want, have := testName, span.Name; want != have {
t.Fatalf("incorrect span name, wanted %s, got %s", want, have)
}
if want, have := model.Client, span.Kind; want != have {
t.Fatalf("incorrect span kind, wanted %s, got %s", want, have)
}
tags := map[string]string{
testTagKey: testTagValue,
string(zipkin.TagHTTPStatusCode): fmt.Sprint(responseStatusCode),
string(zipkin.TagHTTPMethod): rMethod,
string(zipkin.TagHTTPUrl): rURL.String(),
string(zipkin.TagHTTPResponseSize): fmt.Sprint(len(testBody)),
}
if errTagValue != "" {
tags[string(zipkin.TagError)] = fmt.Sprint(errTagValue)
}
if !reflect.DeepEqual(span.Tags, tags) {
t.Fatalf("invalid tags set, wanted %+v, got %+v", tags, span.Tags)
}
}
func TestHTTPServerTrace(t *testing.T) {
rec := recorder.NewReporter()
defer rec.Close()
// explicitly show we use the default of RPC shared spans in Zipkin as it
// is idiomatic for Zipkin to share span identifiers between client and
// server side.
tr, _ := zipkin.NewTracer(rec, zipkin.WithSharedSpans(true))
handler := kithttp.NewServer(
endpoint.Nop,
func(context.Context, *http.Request) (interface{}, error) { return nil, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return errors.New("dummy") },
zipkinkit.HTTPServerTrace(tr),
)
server := httptest.NewServer(handler)
defer server.Close()
const httpMethod = "GET"
req, err := http.NewRequest(httpMethod, server.URL, nil)
if err != nil {
t.Fatalf("unable to create HTTP request: %s", err.Error())
}
parentSpan := tr.StartSpan("Dummy")
b3.InjectHTTP(req)(parentSpan.Context())
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("unable to send HTTP request: %s", err.Error())
}
resp.Body.Close()
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
if want, have := parentSpan.Context().TraceID, spans[0].SpanContext.TraceID; want != have {
t.Errorf("incorrect TraceID, want %+v, have %+v", want, have)
}
if want, have := parentSpan.Context().ID, spans[0].SpanContext.ID; want != have {
t.Errorf("incorrect span ID, want %d, have %d", want, have)
}
if want, have := httpMethod, spans[0].Name; want != have {
t.Errorf("incorrect span name, want %s, have %s", want, have)
}
if want, have := http.StatusText(500), spans[0].Tags["error"]; want != have {
t.Fatalf("incorrect error tag, want %s, have %s", want, have)
}
}
func TestHTTPServerTraceIsRequestBasedSampled(t *testing.T) {
rec := recorder.NewReporter()
defer rec.Close()
const httpMethod = "DELETE"
tr, _ := zipkin.NewTracer(rec)
handler := kithttp.NewServer(
endpoint.Nop,
func(context.Context, *http.Request) (interface{}, error) { return nil, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
zipkinkit.HTTPServerTrace(tr, zipkinkit.RequestSampler(func(r *http.Request) bool {
return r.Method == httpMethod
})),
)
server := httptest.NewServer(handler)
defer server.Close()
req, err := http.NewRequest(httpMethod, server.URL, nil)
if err != nil {
t.Fatalf("unable to create HTTP request: %s", err.Error())
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("unable to send HTTP request: %s", err.Error())
}
resp.Body.Close()
spans := rec.Flush()
if want, have := 1, len(spans); want != have {
t.Fatalf("incorrect number of spans, want %d, have %d", want, have)
}
}