blob: 6bf2ac66f8c81544cf6e2f250d0cfb4b387544f3 [file] [log] [blame]
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES 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 (
"encoding/json"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
"time"
"cloud.google.com/go/trace"
"golang.org/x/net/context"
api "google.golang.org/api/cloudtrace/v1"
"google.golang.org/api/option"
)
const testProjectID = "testproject"
type fakeRoundTripper struct {
reqc chan *http.Request
}
func newFakeRoundTripper() *fakeRoundTripper {
return &fakeRoundTripper{reqc: make(chan *http.Request)}
}
func (rt *fakeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
rt.reqc <- r
resp := &http.Response{
Status: "200 OK",
StatusCode: 200,
Body: ioutil.NopCloser(strings.NewReader("{}")),
}
return resp, nil
}
func newTestClient(rt http.RoundTripper) *trace.Client {
t, err := trace.NewClient(context.Background(), testProjectID, option.WithHTTPClient(&http.Client{Transport: rt}))
if err != nil {
panic(err)
}
return t
}
// makeRequests makes some requests.
// req is an incoming request used to construct the trace. traceClient is the
// client used to upload the trace. rt is the trace client's http client's
// transport. This is used to retrieve the trace uploaded by the client, if
// any. If expectTrace is true, we expect a trace will be uploaded. If
// synchronous is true, the call to Finish is expected not to return before the
// client has uploaded any traces.
func makeRequests(t *testing.T, req *http.Request, traceClient *trace.Client, rt *fakeRoundTripper, synchronous bool, expectTrace bool) *http.Request {
span := traceClient.SpanFromRequest(req)
{
req2, err := http.NewRequest("GET", "http://example.com/bar", nil)
if err != nil {
t.Fatal(err)
}
resp := &http.Response{StatusCode: 200}
s := span.NewRemoteChild(req2)
s.Finish(trace.WithResponse(resp))
}
done := make(chan struct{})
go func() {
if synchronous {
err := span.FinishWait()
if err != nil {
t.Errorf("Unexpected error from span.FinishWait: %v", err)
}
} else {
span.Finish()
}
done <- struct{}{}
}()
if !expectTrace {
<-done
select {
case <-rt.reqc:
t.Errorf("Got a trace, expected none.")
case <-time.After(5 * time.Millisecond):
}
return nil
} else if !synchronous {
<-done
return <-rt.reqc
} else {
select {
case <-done:
t.Errorf("Synchronous Finish didn't wait for trace upload.")
return <-rt.reqc
case <-time.After(5 * time.Millisecond):
r := <-rt.reqc
<-done
return r
}
}
}
func TestTrace(t *testing.T) {
testTrace(t, false)
}
func TestTraceWithWait(t *testing.T) {
testTrace(t, true)
}
func testTrace(t *testing.T, synchronous bool) {
req, err := http.NewRequest("GET", "http://example.com/foo", nil)
if err != nil {
t.Fatal(err)
}
req.Header["X-Cloud-Trace-Context"] = []string{`0123456789ABCDEF0123456789ABCDEF/42;o=3`}
rt := newFakeRoundTripper()
traceClient := newTestClient(rt)
uploaded := makeRequests(t, req, traceClient, rt, synchronous, true)
if uploaded == nil {
t.Fatalf("No trace uploaded, expected one.")
}
expected := api.Traces{
Traces: []*api.Trace{
&api.Trace{
ProjectId: testProjectID,
Spans: []*api.TraceSpan{
&api.TraceSpan{
Kind: "RPC_CLIENT",
Labels: map[string]string{
"trace.cloud.google.com/http/host": "example.com",
"trace.cloud.google.com/http/method": "GET",
"trace.cloud.google.com/http/status_code": "200",
"trace.cloud.google.com/http/url": "http://example.com/bar",
},
Name: "/bar",
},
&api.TraceSpan{
Kind: "RPC_SERVER",
Labels: map[string]string{
"trace.cloud.google.com/http/host": "example.com",
"trace.cloud.google.com/http/method": "GET",
"trace.cloud.google.com/http/url": "http://example.com/foo",
},
Name: "/foo",
},
},
TraceId: "0123456789ABCDEF0123456789ABCDEF",
},
},
}
body, err := ioutil.ReadAll(uploaded.Body)
if err != nil {
t.Fatal(err)
}
var patch api.Traces
err = json.Unmarshal(body, &patch)
if err != nil {
t.Fatal(err)
}
if len(patch.Traces) != len(expected.Traces) || len(patch.Traces[0].Spans) != len(expected.Traces[0].Spans) {
got, _ := json.Marshal(patch)
want, _ := json.Marshal(expected)
t.Fatalf("PatchTraces request: got %s want %s", got, want)
}
n := len(patch.Traces[0].Spans)
rootSpan := patch.Traces[0].Spans[n-1]
for i, s := range patch.Traces[0].Spans {
if a, b := s.StartTime, s.EndTime; a > b {
t.Errorf("span %d start time is later than its end time (%q, %q)", i, a, b)
}
if a, b := rootSpan.StartTime, s.StartTime; a > b {
t.Errorf("trace start time is later than span %d start time (%q, %q)", i, a, b)
}
if a, b := s.EndTime, rootSpan.EndTime; a > b {
t.Errorf("span %d end time is later than trace end time (%q, %q)", i, a, b)
}
if i > 1 && i < n-1 {
if a, b := patch.Traces[0].Spans[i-1].EndTime, s.StartTime; a > b {
t.Errorf("span %d end time is later than span %d start time (%q, %q)", i-1, i, a, b)
}
}
}
if x := rootSpan.ParentSpanId; x != 42 {
t.Errorf("Incorrect ParentSpanId: got %d want %d", x, 42)
}
for i, s := range patch.Traces[0].Spans {
if x, y := rootSpan.SpanId, s.ParentSpanId; i < n-1 && x != y {
t.Errorf("Incorrect ParentSpanId in span %d: got %d want %d", i, y, x)
}
}
for i, s := range patch.Traces[0].Spans {
s.EndTime = ""
labels := &expected.Traces[0].Spans[i].Labels
for key, value := range *labels {
if v, ok := s.Labels[key]; !ok {
t.Errorf("Span %d is missing Label %q:%q", i, key, value)
} else if v != value {
t.Errorf("Span %d Label %q: got value %q want %q", i, key, v, value)
}
}
for key, _ := range s.Labels {
if _, ok := (*labels)[key]; key != "trace.cloud.google.com/stacktrace" && !ok {
t.Errorf("Span %d: unexpected label %q", i, key)
}
}
*labels = nil
s.Labels = nil
s.ParentSpanId = 0
if s.SpanId == 0 {
t.Errorf("Incorrect SpanId: got 0 want nonzero")
}
s.SpanId = 0
s.StartTime = ""
}
if !reflect.DeepEqual(patch, expected) {
got, _ := json.Marshal(patch)
want, _ := json.Marshal(expected)
t.Errorf("PatchTraces request: got %s want %s", got, want)
}
}
func TestNoTrace(t *testing.T) {
testNoTrace(t, false)
}
func TestNoTraceWithWait(t *testing.T) {
testNoTrace(t, true)
}
func testNoTrace(t *testing.T, synchronous bool) {
for _, header := range []string{
`0123456789ABCDEF0123456789ABCDEF/42;o=2`,
`0123456789ABCDEF0123456789ABCDEF/42;o=0`,
`0123456789ABCDEF0123456789ABCDEF/42`,
`0123456789ABCDEF0123456789ABCDEF`,
``,
} {
req, err := http.NewRequest("GET", "http://example.com/foo", nil)
if header != "" {
req.Header["X-Cloud-Trace-Context"] = []string{header}
}
if err != nil {
t.Fatal(err)
}
rt := newFakeRoundTripper()
traceClient := newTestClient(rt)
uploaded := makeRequests(t, req, traceClient, rt, synchronous, false)
if uploaded != nil {
t.Errorf("Got a trace, expected none.")
}
}
}