1// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package ocagent_test
6
7import (
8	"bytes"
9	"context"
10	"encoding/json"
11	"fmt"
12	"io/ioutil"
13	"net/http"
14	"sync"
15	"testing"
16	"time"
17
18	"golang.org/x/tools/internal/event"
19	"golang.org/x/tools/internal/event/core"
20	"golang.org/x/tools/internal/event/export"
21	"golang.org/x/tools/internal/event/export/metric"
22	"golang.org/x/tools/internal/event/export/ocagent"
23	"golang.org/x/tools/internal/event/keys"
24	"golang.org/x/tools/internal/event/label"
25)
26
27const testNodeStr = `{
28	"node":{
29		"identifier":{
30			"host_name":"tester",
31			"pid":1,
32			"start_timestamp":"1970-01-01T00:00:00Z"
33		},
34		"library_info":{
35			"language":4,
36			"exporter_version":"0.0.1",
37			"core_library_version":"x/tools"
38		},
39		"service_info":{
40			"name":"ocagent-tests"
41		}
42	},`
43
44var (
45	keyDB     = keys.NewString("db", "the database name")
46	keyMethod = keys.NewString("method", "a metric grouping key")
47	keyRoute  = keys.NewString("route", "another metric grouping key")
48
49	key1DB = keys.NewString("1_db", "A test string key")
50
51	key2aAge      = keys.NewFloat64("2a_age", "A test float64 key")
52	key2bTTL      = keys.NewFloat32("2b_ttl", "A test float32 key")
53	key2cExpiryMS = keys.NewFloat64("2c_expiry_ms", "A test float64 key")
54
55	key3aRetry = keys.NewBoolean("3a_retry", "A test boolean key")
56	key3bStale = keys.NewBoolean("3b_stale", "Another test boolean key")
57
58	key4aMax      = keys.NewInt("4a_max", "A test int key")
59	key4bOpcode   = keys.NewInt8("4b_opcode", "A test int8 key")
60	key4cBase     = keys.NewInt16("4c_base", "A test int16 key")
61	key4eChecksum = keys.NewInt32("4e_checksum", "A test int32 key")
62	key4fMode     = keys.NewInt64("4f_mode", "A test int64 key")
63
64	key5aMin     = keys.NewUInt("5a_min", "A test uint key")
65	key5bMix     = keys.NewUInt8("5b_mix", "A test uint8 key")
66	key5cPort    = keys.NewUInt16("5c_port", "A test uint16 key")
67	key5dMinHops = keys.NewUInt32("5d_min_hops", "A test uint32 key")
68	key5eMaxHops = keys.NewUInt64("5e_max_hops", "A test uint64 key")
69
70	recursiveCalls = keys.NewInt64("recursive_calls", "Number of recursive calls")
71	bytesIn        = keys.NewInt64("bytes_in", "Number of bytes in")           //, unit.Bytes)
72	latencyMs      = keys.NewFloat64("latency", "The latency in milliseconds") //, unit.Milliseconds)
73
74	metricLatency = metric.HistogramFloat64{
75		Name:        "latency_ms",
76		Description: "The latency of calls in milliseconds",
77		Keys:        []label.Key{keyMethod, keyRoute},
78		Buckets:     []float64{0, 5, 10, 25, 50},
79	}
80
81	metricBytesIn = metric.HistogramInt64{
82		Name:        "latency_ms",
83		Description: "The latency of calls in milliseconds",
84		Keys:        []label.Key{keyMethod, keyRoute},
85		Buckets:     []int64{0, 10, 50, 100, 500, 1000, 2000},
86	}
87
88	metricRecursiveCalls = metric.Scalar{
89		Name:        "latency_ms",
90		Description: "The latency of calls in milliseconds",
91		Keys:        []label.Key{keyMethod, keyRoute},
92	}
93)
94
95type testExporter struct {
96	ocagent *ocagent.Exporter
97	sent    fakeSender
98}
99
100func registerExporter() *testExporter {
101	exporter := &testExporter{}
102	cfg := ocagent.Config{
103		Host:    "tester",
104		Process: 1,
105		Service: "ocagent-tests",
106		Client:  &http.Client{Transport: &exporter.sent},
107	}
108	cfg.Start, _ = time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00Z")
109	exporter.ocagent = ocagent.Connect(&cfg)
110
111	metrics := metric.Config{}
112	metricLatency.Record(&metrics, latencyMs)
113	metricBytesIn.Record(&metrics, bytesIn)
114	metricRecursiveCalls.SumInt64(&metrics, recursiveCalls)
115
116	e := exporter.ocagent.ProcessEvent
117	e = metrics.Exporter(e)
118	e = spanFixer(e)
119	e = export.Spans(e)
120	e = export.Labels(e)
121	e = timeFixer(e)
122	event.SetExporter(e)
123	return exporter
124}
125
126func timeFixer(output event.Exporter) event.Exporter {
127	start, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:30Z")
128	at, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:40Z")
129	end, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:50Z")
130	return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
131		switch {
132		case event.IsStart(ev):
133			ev = core.CloneEvent(ev, start)
134		case event.IsEnd(ev):
135			ev = core.CloneEvent(ev, end)
136		default:
137			ev = core.CloneEvent(ev, at)
138		}
139		return output(ctx, ev, lm)
140	}
141}
142
143func spanFixer(output event.Exporter) event.Exporter {
144	return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
145		if event.IsStart(ev) {
146			span := export.GetSpan(ctx)
147			span.ID = export.SpanContext{}
148		}
149		return output(ctx, ev, lm)
150	}
151}
152
153func (e *testExporter) Output(route string) []byte {
154	e.ocagent.Flush()
155	return e.sent.get(route)
156}
157
158func checkJSON(t *testing.T, got, want []byte) {
159	// compare the compact form, to allow for formatting differences
160	g := &bytes.Buffer{}
161	if err := json.Compact(g, got); err != nil {
162		t.Fatal(err)
163	}
164	w := &bytes.Buffer{}
165	if err := json.Compact(w, want); err != nil {
166		t.Fatal(err)
167	}
168	if g.String() != w.String() {
169		t.Fatalf("Got:\n%s\nWant:\n%s", g, w)
170	}
171}
172
173type fakeSender struct {
174	mu   sync.Mutex
175	data map[string][]byte
176}
177
178func (s *fakeSender) get(route string) []byte {
179	s.mu.Lock()
180	defer s.mu.Unlock()
181	data, found := s.data[route]
182	if found {
183		delete(s.data, route)
184	}
185	return data
186}
187
188func (s *fakeSender) RoundTrip(req *http.Request) (*http.Response, error) {
189	s.mu.Lock()
190	defer s.mu.Unlock()
191	if s.data == nil {
192		s.data = make(map[string][]byte)
193	}
194	data, err := ioutil.ReadAll(req.Body)
195	if err != nil {
196		return nil, err
197	}
198	path := req.URL.EscapedPath()
199	if _, found := s.data[path]; found {
200		return nil, fmt.Errorf("duplicate delivery to %v", path)
201	}
202	s.data[path] = data
203	return &http.Response{
204		Status:     "200 OK",
205		StatusCode: 200,
206		Proto:      "HTTP/1.0",
207		ProtoMajor: 1,
208		ProtoMinor: 0,
209	}, nil
210}
211