// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package ocagent_test import ( "bytes" "context" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "sync" "testing" "time" "golang.org/x/tools/internal/telemetry" "golang.org/x/tools/internal/telemetry/export" "golang.org/x/tools/internal/telemetry/export/ocagent" "golang.org/x/tools/internal/telemetry/tag" ) var ( exporter export.Exporter sent fakeSender start time.Time at time.Time end time.Time ) func init() { cfg := ocagent.Config{ Host: "tester", Process: 1, Service: "ocagent-tests", Client: &http.Client{Transport: &sent}, } cfg.Start, _ = time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00Z") exporter = ocagent.Connect(&cfg) } const testNodeStr = `{ "node":{ "identifier":{ "host_name":"tester", "pid":1, "start_timestamp":"1970-01-01T00:00:00Z" }, "library_info":{ "language":4, "exporter_version":"0.0.1", "core_library_version":"x/tools" }, "service_info":{ "name":"ocagent-tests" } },` func TestEvents(t *testing.T) { start, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:30Z") at, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:40Z") end, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:50Z") const prefix = testNodeStr + ` "spans":[{ "trace_id":"AAAAAAAAAAAAAAAAAAAAAA==", "span_id":"AAAAAAAAAAA=", "parent_span_id":"AAAAAAAAAAA=", "name":{"value":"event span"}, "start_time":"1970-01-01T00:00:30Z", "end_time":"1970-01-01T00:00:50Z", "time_events":{ ` const suffix = ` }, "same_process_as_parent_span":true }] }` tests := []struct { name string event func(ctx context.Context) telemetry.Event want string }{ { name: "no tags", event: func(ctx context.Context) telemetry.Event { return telemetry.Event{ At: at, } }, want: prefix + ` "timeEvent":[{"time":"1970-01-01T00:00:40Z"}] ` + suffix, }, { name: "description no error", event: func(ctx context.Context) telemetry.Event { return telemetry.Event{ At: at, Message: "cache miss", Tags: telemetry.TagList{ tag.Of("db", "godb"), }, } }, want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{ "description": { "value": "cache miss" }, "attributes": { "attributeMap": { "db": { "stringValue": { "value": "godb" } } } } }}]` + suffix, }, { name: "description and error", event: func(ctx context.Context) telemetry.Event { return telemetry.Event{ At: at, Message: "cache miss", Error: errors.New("no network connectivity"), Tags: telemetry.TagList{ tag.Of("db", "godb"), // must come before e }, } }, want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{ "description": { "value": "cache miss" }, "attributes": { "attributeMap": { "db": { "stringValue": { "value": "godb" } }, "error": { "stringValue": { "value": "no network connectivity" } } } } }}]` + suffix, }, { name: "no description, but error", event: func(ctx context.Context) telemetry.Event { return telemetry.Event{ At: at, Error: errors.New("no network connectivity"), Tags: telemetry.TagList{ tag.Of("db", "godb"), }, } }, want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{ "description": { "value": "no network connectivity" }, "attributes": { "attributeMap": { "db": { "stringValue": { "value": "godb" } } } } }}]` + suffix, }, { name: "enumerate all attribute types", event: func(ctx context.Context) telemetry.Event { return telemetry.Event{ At: at, Message: "cache miss", Tags: telemetry.TagList{ tag.Of("1_db", "godb"), tag.Of("2a_age", 0.456), // Constant converted into "float64" tag.Of("2b_ttl", float32(5000)), tag.Of("2c_expiry_ms", float64(1e3)), tag.Of("3a_retry", false), tag.Of("3b_stale", true), tag.Of("4a_max", 0x7fff), // Constant converted into "int" tag.Of("4b_opcode", int8(0x7e)), tag.Of("4c_base", int16(1<<9)), tag.Of("4e_checksum", int32(0x11f7e294)), tag.Of("4f_mode", int64(0644)), tag.Of("5a_min", uint(1)), tag.Of("5b_mix", uint8(44)), tag.Of("5c_port", uint16(55678)), tag.Of("5d_min_hops", uint32(1<<9)), tag.Of("5e_max_hops", uint64(0xffffff)), }, } }, want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{ "description": { "value": "cache miss" }, "attributes": { "attributeMap": { "1_db": { "stringValue": { "value": "godb" } }, "2a_age": { "doubleValue": 0.456 }, "2b_ttl": { "doubleValue": 5000 }, "2c_expiry_ms": { "doubleValue": 1000 }, "3a_retry": {}, "3b_stale": { "boolValue": true }, "4a_max": { "intValue": 32767 }, "4b_opcode": { "intValue": 126 }, "4c_base": { "intValue": 512 }, "4e_checksum": { "intValue": 301458068 }, "4f_mode": { "intValue": 420 }, "5a_min": { "intValue": 1 }, "5b_mix": { "intValue": 44 }, "5c_port": { "intValue": 55678 }, "5d_min_hops": { "intValue": 512 }, "5e_max_hops": { "intValue": 16777215 } } } }}]` + suffix, }, } ctx := context.TODO() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { span := &telemetry.Span{ Name: "event span", Start: start, Finish: end, Events: []telemetry.Event{tt.event(ctx)}, } exporter.StartSpan(ctx, span) exporter.FinishSpan(ctx, span) exporter.Flush() got := sent.get("/v1/trace") checkJSON(t, got, []byte(tt.want)) }) } } func checkJSON(t *testing.T, got, want []byte) { // compare the compact form, to allow for formatting differences g := &bytes.Buffer{} if err := json.Compact(g, got); err != nil { t.Fatal(err) } w := &bytes.Buffer{} if err := json.Compact(w, want); err != nil { t.Fatal(err) } if g.String() != w.String() { t.Fatalf("Got:\n%s\nWant:\n%s", g, w) } } type fakeSender struct { mu sync.Mutex data map[string][]byte } func (s *fakeSender) get(route string) []byte { s.mu.Lock() defer s.mu.Unlock() data, found := s.data[route] if found { delete(s.data, route) } return data } func (s *fakeSender) RoundTrip(req *http.Request) (*http.Response, error) { s.mu.Lock() defer s.mu.Unlock() if s.data == nil { s.data = make(map[string][]byte) } data, err := ioutil.ReadAll(req.Body) if err != nil { return nil, err } path := req.URL.EscapedPath() if _, found := s.data[path]; found { return nil, fmt.Errorf("duplicate delivery to %v", path) } s.data[path] = data return &http.Response{ Status: "200 OK", StatusCode: 200, Proto: "HTTP/1.0", ProtoMajor: 1, ProtoMinor: 0, }, nil }