|  | // 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" | 
|  | "fmt" | 
|  | "io" | 
|  | "net/http" | 
|  | "sync" | 
|  | "testing" | 
|  | "time" | 
|  |  | 
|  | "golang.org/x/tools/internal/event" | 
|  | "golang.org/x/tools/internal/event/core" | 
|  | "golang.org/x/tools/internal/event/export" | 
|  | "golang.org/x/tools/internal/event/export/metric" | 
|  | "golang.org/x/tools/internal/event/export/ocagent" | 
|  | "golang.org/x/tools/internal/event/keys" | 
|  | "golang.org/x/tools/internal/event/label" | 
|  | ) | 
|  |  | 
|  | 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" | 
|  | } | 
|  | },` | 
|  |  | 
|  | var ( | 
|  | keyDB     = keys.NewString("db", "the database name") | 
|  | keyMethod = keys.NewString("method", "a metric grouping key") | 
|  | keyRoute  = keys.NewString("route", "another metric grouping key") | 
|  |  | 
|  | key1DB = keys.NewString("1_db", "A test string key") | 
|  |  | 
|  | key2aAge      = keys.NewFloat64("2a_age", "A test float64 key") | 
|  | key2bTTL      = keys.NewFloat32("2b_ttl", "A test float32 key") | 
|  | key2cExpiryMS = keys.NewFloat64("2c_expiry_ms", "A test float64 key") | 
|  |  | 
|  | key3aRetry = keys.NewBoolean("3a_retry", "A test boolean key") | 
|  | key3bStale = keys.NewBoolean("3b_stale", "Another test boolean key") | 
|  |  | 
|  | key4aMax      = keys.NewInt("4a_max", "A test int key") | 
|  | key4bOpcode   = keys.NewInt8("4b_opcode", "A test int8 key") | 
|  | key4cBase     = keys.NewInt16("4c_base", "A test int16 key") | 
|  | key4eChecksum = keys.NewInt32("4e_checksum", "A test int32 key") | 
|  | key4fMode     = keys.NewInt64("4f_mode", "A test int64 key") | 
|  |  | 
|  | key5aMin     = keys.NewUInt("5a_min", "A test uint key") | 
|  | key5bMix     = keys.NewUInt8("5b_mix", "A test uint8 key") | 
|  | key5cPort    = keys.NewUInt16("5c_port", "A test uint16 key") | 
|  | key5dMinHops = keys.NewUInt32("5d_min_hops", "A test uint32 key") | 
|  | key5eMaxHops = keys.NewUInt64("5e_max_hops", "A test uint64 key") | 
|  |  | 
|  | recursiveCalls = keys.NewInt64("recursive_calls", "Number of recursive calls") | 
|  | bytesIn        = keys.NewInt64("bytes_in", "Number of bytes in")           //, unit.Bytes) | 
|  | latencyMs      = keys.NewFloat64("latency", "The latency in milliseconds") //, unit.Milliseconds) | 
|  |  | 
|  | metricLatency = metric.HistogramFloat64{ | 
|  | Name:        "latency_ms", | 
|  | Description: "The latency of calls in milliseconds", | 
|  | Keys:        []label.Key{keyMethod, keyRoute}, | 
|  | Buckets:     []float64{0, 5, 10, 25, 50}, | 
|  | } | 
|  |  | 
|  | metricBytesIn = metric.HistogramInt64{ | 
|  | Name:        "latency_ms", | 
|  | Description: "The latency of calls in milliseconds", | 
|  | Keys:        []label.Key{keyMethod, keyRoute}, | 
|  | Buckets:     []int64{0, 10, 50, 100, 500, 1000, 2000}, | 
|  | } | 
|  |  | 
|  | metricRecursiveCalls = metric.Scalar{ | 
|  | Name:        "latency_ms", | 
|  | Description: "The latency of calls in milliseconds", | 
|  | Keys:        []label.Key{keyMethod, keyRoute}, | 
|  | } | 
|  | ) | 
|  |  | 
|  | type testExporter struct { | 
|  | ocagent *ocagent.Exporter | 
|  | sent    fakeSender | 
|  | } | 
|  |  | 
|  | func registerExporter() *testExporter { | 
|  | exporter := &testExporter{} | 
|  | cfg := ocagent.Config{ | 
|  | Host:    "tester", | 
|  | Process: 1, | 
|  | Service: "ocagent-tests", | 
|  | Client:  &http.Client{Transport: &exporter.sent}, | 
|  | } | 
|  | cfg.Start, _ = time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00Z") | 
|  | exporter.ocagent = ocagent.Connect(&cfg) | 
|  |  | 
|  | metrics := metric.Config{} | 
|  | metricLatency.Record(&metrics, latencyMs) | 
|  | metricBytesIn.Record(&metrics, bytesIn) | 
|  | metricRecursiveCalls.SumInt64(&metrics, recursiveCalls) | 
|  |  | 
|  | e := exporter.ocagent.ProcessEvent | 
|  | e = metrics.Exporter(e) | 
|  | e = spanFixer(e) | 
|  | e = export.Spans(e) | 
|  | e = export.Labels(e) | 
|  | e = timeFixer(e) | 
|  | event.SetExporter(e) | 
|  | return exporter | 
|  | } | 
|  |  | 
|  | func timeFixer(output event.Exporter) event.Exporter { | 
|  | 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") | 
|  | return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { | 
|  | switch { | 
|  | case event.IsStart(ev): | 
|  | ev = core.CloneEvent(ev, start) | 
|  | case event.IsEnd(ev): | 
|  | ev = core.CloneEvent(ev, end) | 
|  | default: | 
|  | ev = core.CloneEvent(ev, at) | 
|  | } | 
|  | return output(ctx, ev, lm) | 
|  | } | 
|  | } | 
|  |  | 
|  | func spanFixer(output event.Exporter) event.Exporter { | 
|  | return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { | 
|  | if event.IsStart(ev) { | 
|  | span := export.GetSpan(ctx) | 
|  | span.ID = export.SpanContext{} | 
|  | } | 
|  | return output(ctx, ev, lm) | 
|  | } | 
|  | } | 
|  |  | 
|  | func (e *testExporter) Output(route string) []byte { | 
|  | e.ocagent.Flush() | 
|  | return e.sent.get(route) | 
|  | } | 
|  |  | 
|  | 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 := io.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 | 
|  | } |