123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685 |
- // 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 (
- "bytes"
- "context"
- "encoding/hex"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net"
- "net/http"
- "net/http/httptest"
- "net/url"
- "reflect"
- "strings"
- "testing"
- "time"
- "go.opencensus.io/plugin/ochttp/propagation/b3"
- "go.opencensus.io/plugin/ochttp/propagation/tracecontext"
- "go.opencensus.io/trace"
- )
- type testExporter struct {
- spans []*trace.SpanData
- }
- func (t *testExporter) ExportSpan(s *trace.SpanData) {
- t.spans = append(t.spans, s)
- }
- type testTransport struct {
- ch chan *http.Request
- }
- func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- t.ch <- req
- return nil, errors.New("noop")
- }
- type testPropagator struct{}
- func (t testPropagator) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) {
- header := req.Header.Get("trace")
- buf, err := hex.DecodeString(header)
- if err != nil {
- log.Fatalf("Cannot decode trace header: %q", header)
- }
- r := bytes.NewReader(buf)
- r.Read(sc.TraceID[:])
- r.Read(sc.SpanID[:])
- opts, err := r.ReadByte()
- if err != nil {
- log.Fatalf("Cannot read trace options from trace header: %q", header)
- }
- sc.TraceOptions = trace.TraceOptions(opts)
- return sc, true
- }
- func (t testPropagator) SpanContextToRequest(sc trace.SpanContext, req *http.Request) {
- var buf bytes.Buffer
- buf.Write(sc.TraceID[:])
- buf.Write(sc.SpanID[:])
- buf.WriteByte(byte(sc.TraceOptions))
- 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) {
- _, parent := trace.StartSpan(context.Background(), "parent")
- tests := []struct {
- name string
- parent *trace.Span
- }{
- {
- name: "no parent",
- parent: nil,
- },
- {
- name: "parent",
- parent: parent,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- transport := &testTransport{ch: make(chan *http.Request, 1)}
- rt := &Transport{
- Propagation: &testPropagator{},
- Base: transport,
- }
- req, _ := http.NewRequest("GET", "http://foo.com", nil)
- if tt.parent != nil {
- req = req.WithContext(trace.NewContext(req.Context(), tt.parent))
- }
- rt.RoundTrip(req)
- req = <-transport.ch
- span := trace.FromContext(req.Context())
- if header := req.Header.Get("trace"); header == "" {
- t.Fatalf("Trace header = empty; want valid trace header")
- }
- if span == nil {
- t.Fatalf("Got no spans in req context; want one")
- }
- if tt.parent != nil {
- if got, want := span.SpanContext().TraceID, tt.parent.SpanContext().TraceID; got != want {
- t.Errorf("span.SpanContext().TraceID=%v; want %v", got, want)
- }
- }
- })
- }
- }
- func TestHandler(t *testing.T) {
- traceID := [16]byte{16, 84, 69, 170, 120, 67, 188, 139, 242, 6, 177, 32, 0, 16, 0, 0}
- tests := []struct {
- header string
- wantTraceID trace.TraceID
- wantTraceOptions trace.TraceOptions
- }{
- {
- header: "105445aa7843bc8bf206b12000100000000000000000000000",
- wantTraceID: traceID,
- wantTraceOptions: trace.TraceOptions(0),
- },
- {
- header: "105445aa7843bc8bf206b12000100000000000000000000001",
- wantTraceID: traceID,
- wantTraceOptions: trace.TraceOptions(1),
- },
- }
- for _, tt := range tests {
- t.Run(tt.header, func(t *testing.T) {
- handler := &Handler{
- Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- span := trace.FromContext(r.Context())
- sc := span.SpanContext()
- if got, want := sc.TraceID, tt.wantTraceID; got != want {
- t.Errorf("TraceID = %q; want %q", got, want)
- }
- if got, want := sc.TraceOptions, tt.wantTraceOptions; got != want {
- t.Errorf("TraceOptions = %v; want %v", got, want)
- }
- }),
- StartOptions: trace.StartOptions{Sampler: trace.ProbabilitySampler(0.0)},
- Propagation: &testPropagator{},
- }
- req, _ := http.NewRequest("GET", "http://foo.com", nil)
- req.Header.Add("trace", tt.header)
- handler.ServeHTTP(nil, req)
- })
- }
- }
- var _ http.RoundTripper = (*traceTransport)(nil)
- type collector []*trace.SpanData
- func (c *collector) ExportSpan(s *trace.SpanData) {
- *c = append(*c, s)
- }
- func TestEndToEnd(t *testing.T) {
- tc := []struct {
- name string
- handler *Handler
- transport *Transport
- wantSameTraceID bool
- wantLinks bool // expect a link between client and server span
- }{
- {
- name: "internal default propagation",
- handler: &Handler{},
- transport: &Transport{},
- wantSameTraceID: true,
- },
- {
- name: "external default propagation",
- handler: &Handler{IsPublicEndpoint: true},
- transport: &Transport{},
- wantSameTraceID: false,
- wantLinks: true,
- },
- {
- name: "internal TraceContext propagation",
- handler: &Handler{Propagation: &tracecontext.HTTPFormat{}},
- transport: &Transport{Propagation: &tracecontext.HTTPFormat{}},
- wantSameTraceID: true,
- },
- {
- name: "misconfigured propagation",
- handler: &Handler{IsPublicEndpoint: true, Propagation: &tracecontext.HTTPFormat{}},
- transport: &Transport{Propagation: &b3.HTTPFormat{}},
- wantSameTraceID: false,
- wantLinks: false,
- },
- }
- for _, tt := range tc {
- t.Run(tt.name, func(t *testing.T) {
- var spans collector
- trace.RegisterExporter(&spans)
- defer trace.UnregisterExporter(&spans)
- // Start the server.
- serverDone := make(chan struct{})
- serverReturn := make(chan time.Time)
- tt.handler.StartOptions.Sampler = trace.AlwaysSample()
- url := serveHTTP(tt.handler, serverDone, serverReturn, 200)
- ctx := context.Background()
- // Make the request.
- req, err := http.NewRequest(
- http.MethodPost,
- fmt.Sprintf("%s/example/url/path?qparam=val", url),
- strings.NewReader("expected-request-body"))
- if err != nil {
- t.Fatal(err)
- }
- req = req.WithContext(ctx)
- tt.transport.StartOptions.Sampler = trace.AlwaysSample()
- c := &http.Client{
- Transport: tt.transport,
- }
- resp, err := c.Do(req)
- if err != nil {
- t.Fatal(err)
- }
- if resp.StatusCode != http.StatusOK {
- t.Fatalf("resp.StatusCode = %d", resp.StatusCode)
- }
- // Tell the server to return from request handling.
- serverReturn <- time.Now().Add(time.Millisecond)
- respBody, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- t.Fatal(err)
- }
- if got, want := string(respBody), "expected-response"; got != want {
- t.Fatalf("respBody = %q; want %q", got, want)
- }
- resp.Body.Close()
- <-serverDone
- trace.UnregisterExporter(&spans)
- if got, want := len(spans), 2; got != want {
- t.Fatalf("len(spans) = %d; want %d", got, want)
- }
- var client, server *trace.SpanData
- for _, sp := range spans {
- switch sp.SpanKind {
- case trace.SpanKindClient:
- client = sp
- if got, want := client.Name, "/example/url/path"; got != want {
- t.Errorf("Span name: %q; want %q", got, want)
- }
- case trace.SpanKindServer:
- server = sp
- if got, want := server.Name, "/example/url/path"; got != want {
- t.Errorf("Span name: %q; want %q", got, want)
- }
- default:
- t.Fatalf("server or client span missing; kind = %v", sp.SpanKind)
- }
- }
- if tt.wantSameTraceID {
- if server.TraceID != client.TraceID {
- t.Errorf("TraceID does not match: server.TraceID=%q client.TraceID=%q", server.TraceID, client.TraceID)
- }
- if !server.HasRemoteParent {
- t.Errorf("server span should have remote parent")
- }
- if server.ParentSpanID != client.SpanID {
- t.Errorf("server span should have client span as parent")
- }
- }
- if !tt.wantSameTraceID {
- if server.TraceID == client.TraceID {
- t.Errorf("TraceID should not be trusted")
- }
- }
- if tt.wantLinks {
- if got, want := len(server.Links), 1; got != want {
- t.Errorf("len(server.Links) = %d; want %d", got, want)
- } else {
- link := server.Links[0]
- if got, want := link.Type, trace.LinkTypeParent; got != want {
- t.Errorf("link.Type = %v; want %v", got, want)
- }
- }
- }
- if server.StartTime.Before(client.StartTime) {
- t.Errorf("server span starts before client span")
- }
- if server.EndTime.After(client.EndTime) {
- t.Errorf("client span ends before server span")
- }
- })
- }
- }
- func serveHTTP(handler *Handler, done chan struct{}, wait chan time.Time, statusCode int) string {
- handler.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(statusCode)
- w.(http.Flusher).Flush()
- // Simulate a slow-responding server.
- sleepUntil := <-wait
- for time.Now().Before(sleepUntil) {
- time.Sleep(sleepUntil.Sub(time.Now()))
- }
- io.WriteString(w, "expected-response")
- close(done)
- })
- server := httptest.NewServer(handler)
- go func() {
- <-done
- server.Close()
- }()
- return server.URL
- }
- func TestSpanNameFromURL(t *testing.T) {
- tests := []struct {
- u string
- want string
- }{
- {
- u: "http://localhost:80/hello?q=a",
- want: "/hello",
- },
- {
- u: "/a/b?q=c",
- want: "/a/b",
- },
- }
- for _, tt := range tests {
- t.Run(tt.u, func(t *testing.T) {
- req, err := http.NewRequest("GET", tt.u, nil)
- if err != nil {
- t.Errorf("url issue = %v", err)
- }
- if got := spanNameFromURL(req); got != tt.want {
- t.Errorf("spanNameFromURL() = %v, want %v", got, tt.want)
- }
- })
- }
- }
- func TestFormatSpanName(t *testing.T) {
- formatSpanName := func(r *http.Request) string {
- return r.Method + " " + r.URL.Path
- }
- handler := &Handler{
- Handler: http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
- resp.Write([]byte("Hello, world!"))
- }),
- FormatSpanName: formatSpanName,
- }
- server := httptest.NewServer(handler)
- defer server.Close()
- client := &http.Client{
- Transport: &Transport{
- FormatSpanName: formatSpanName,
- StartOptions: trace.StartOptions{
- Sampler: trace.AlwaysSample(),
- },
- },
- }
- tests := []struct {
- u string
- want string
- }{
- {
- u: "/hello?q=a",
- want: "GET /hello",
- },
- {
- u: "/a/b?q=c",
- want: "GET /a/b",
- },
- }
- for _, tt := range tests {
- t.Run(tt.u, func(t *testing.T) {
- var te testExporter
- trace.RegisterExporter(&te)
- res, err := client.Get(server.URL + tt.u)
- if err != nil {
- t.Fatalf("error creating request: %v", err)
- }
- res.Body.Close()
- trace.UnregisterExporter(&te)
- if want, got := 2, len(te.spans); want != got {
- t.Fatalf("got exported spans %#v, wanted two spans", te.spans)
- }
- if got := te.spans[0].Name; got != tt.want {
- t.Errorf("spanNameFromURL() = %v, want %v", got, tt.want)
- }
- if got := te.spans[1].Name; got != tt.want {
- t.Errorf("spanNameFromURL() = %v, want %v", got, tt.want)
- }
- })
- }
- }
- func TestRequestAttributes(t *testing.T) {
- tests := []struct {
- name string
- makeReq func() *http.Request
- wantAttrs []trace.Attribute
- }{
- {
- name: "GET example.com/hello",
- makeReq: func() *http.Request {
- req, _ := http.NewRequest("GET", "http://example.com:779/hello", nil)
- req.Header.Add("User-Agent", "ua")
- return req
- },
- wantAttrs: []trace.Attribute{
- trace.StringAttribute("http.path", "/hello"),
- trace.StringAttribute("http.url", "http://example.com:779/hello"),
- trace.StringAttribute("http.host", "example.com:779"),
- trace.StringAttribute("http.method", "GET"),
- trace.StringAttribute("http.user_agent", "ua"),
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := tt.makeReq()
- attrs := requestAttrs(req)
- if got, want := attrs, tt.wantAttrs; !reflect.DeepEqual(got, want) {
- t.Errorf("Request attributes = %#v; want %#v", got, want)
- }
- })
- }
- }
- func TestResponseAttributes(t *testing.T) {
- tests := []struct {
- name string
- resp *http.Response
- wantAttrs []trace.Attribute
- }{
- {
- name: "non-zero HTTP 200 response",
- resp: &http.Response{StatusCode: 200},
- wantAttrs: []trace.Attribute{
- trace.Int64Attribute("http.status_code", 200),
- },
- },
- {
- name: "zero HTTP 500 response",
- resp: &http.Response{StatusCode: 500},
- wantAttrs: []trace.Attribute{
- trace.Int64Attribute("http.status_code", 500),
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- attrs := responseAttrs(tt.resp)
- if got, want := attrs, tt.wantAttrs; !reflect.DeepEqual(got, want) {
- t.Errorf("Response attributes = %#v; want %#v", got, want)
- }
- })
- }
- }
- type TestCase struct {
- Name string
- Method string
- URL string
- Headers map[string]string
- ResponseCode int
- SpanName string
- SpanStatus string
- SpanKind string
- SpanAttributes map[string]string
- }
- func TestAgainstSpecs(t *testing.T) {
- fmt.Println("start")
- dat, err := ioutil.ReadFile("testdata/http-out-test-cases.json")
- if err != nil {
- t.Fatalf("error reading file: %v", err)
- }
- tests := make([]TestCase, 0)
- err = json.Unmarshal(dat, &tests)
- if err != nil {
- t.Fatalf("error parsing json: %v", err)
- }
- trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
- for _, tt := range tests {
- t.Run(tt.Name, func(t *testing.T) {
- var spans collector
- trace.RegisterExporter(&spans)
- defer trace.UnregisterExporter(&spans)
- handler := &Handler{}
- transport := &Transport{}
- serverDone := make(chan struct{})
- serverReturn := make(chan time.Time)
- host := ""
- port := ""
- serverRequired := strings.Contains(tt.URL, "{")
- if serverRequired {
- // Start the server.
- localServerURL := serveHTTP(handler, serverDone, serverReturn, tt.ResponseCode)
- u, _ := url.Parse(localServerURL)
- host, port, _ = net.SplitHostPort(u.Host)
- tt.URL = strings.Replace(tt.URL, "{host}", host, 1)
- tt.URL = strings.Replace(tt.URL, "{port}", port, 1)
- }
- // Start a root Span in the client.
- ctx, _ := trace.StartSpan(
- context.Background(),
- "top-level")
- // Make the request.
- req, err := http.NewRequest(
- tt.Method,
- tt.URL,
- nil)
- for headerName, headerValue := range tt.Headers {
- req.Header.Add(headerName, headerValue)
- }
- if err != nil {
- t.Fatal(err)
- }
- req = req.WithContext(ctx)
- resp, err := transport.RoundTrip(req)
- if err != nil {
- // do not fail. We want to validate DNS issues
- //t.Fatal(err)
- }
- if serverRequired {
- // Tell the server to return from request handling.
- serverReturn <- time.Now().Add(time.Millisecond)
- }
- if resp != nil {
- // If it simply closes body without reading
- // synchronization problem may happen for spans slice.
- // Server span and client span will write themselves
- // at the same time
- ioutil.ReadAll(resp.Body)
- resp.Body.Close()
- if serverRequired {
- <-serverDone
- }
- }
- trace.UnregisterExporter(&spans)
- var client *trace.SpanData
- for _, sp := range spans {
- if sp.SpanKind == trace.SpanKindClient {
- client = sp
- }
- }
- if client.Name != tt.SpanName {
- t.Errorf("span names don't match: expected: %s, actual: %s", tt.SpanName, client.Name)
- }
- spanKindToStr := map[int]string{
- trace.SpanKindClient: "Client",
- trace.SpanKindServer: "Server",
- }
- if !strings.EqualFold(codeToStr[client.Status.Code], tt.SpanStatus) {
- t.Errorf("span status don't match: expected: %s, actual: %d (%s)", tt.SpanStatus, client.Status.Code, codeToStr[client.Status.Code])
- }
- if !strings.EqualFold(spanKindToStr[client.SpanKind], tt.SpanKind) {
- t.Errorf("span kind don't match: expected: %s, actual: %d (%s)", tt.SpanKind, client.SpanKind, spanKindToStr[client.SpanKind])
- }
- normalizedActualAttributes := map[string]string{}
- for k, v := range client.Attributes {
- normalizedActualAttributes[k] = fmt.Sprintf("%v", v)
- }
- normalizedExpectedAttributes := map[string]string{}
- for k, v := range tt.SpanAttributes {
- normalizedValue := v
- normalizedValue = strings.Replace(normalizedValue, "{host}", host, 1)
- normalizedValue = strings.Replace(normalizedValue, "{port}", port, 1)
- normalizedExpectedAttributes[k] = normalizedValue
- }
- if got, want := normalizedActualAttributes, normalizedExpectedAttributes; !reflect.DeepEqual(got, want) {
- t.Errorf("Request attributes = %#v; want %#v", got, want)
- }
- })
- }
- }
- func TestStatusUnitTest(t *testing.T) {
- tests := []struct {
- 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`}},
- }
- for _, tt := range tests {
- got, want := TraceStatus(tt.in, ""), tt.want
- if got != want {
- t.Errorf("status(%d) got = (%#v) want = (%#v)", tt.in, got, want)
- }
- }
- }
|