Browse Source

Add experimental support for tracing with OpenCensus

See `github.com/olivere/trace/opencensus`.

Close #912
Oliver Eilhard 10 months ago
parent
commit
2ddc762ab5
3 changed files with 367 additions and 0 deletions
  1. 222
    0
      trace/opencensus/transport.go
  2. 131
    0
      trace/opencensus/transport_test.go
  3. 14
    0
      trace/opencensus/util.go

+ 222
- 0
trace/opencensus/transport.go View File

@@ -0,0 +1,222 @@
1
+// Copyright 2012-present Oliver Eilhard. All rights reserved.
2
+// Use of this source code is governed by a MIT-license.
3
+// See http://olivere.mit-license.org/license.txt for details.
4
+
5
+package opencensus
6
+
7
+import (
8
+	"context"
9
+	"fmt"
10
+	"net/http"
11
+	"net/url"
12
+
13
+	"github.com/pkg/errors"
14
+	"go.opencensus.io/trace"
15
+)
16
+
17
+// Transport for tracing Elastic operations.
18
+type Transport struct {
19
+	rt                http.RoundTripper
20
+	defaultAttributes []trace.Attribute
21
+}
22
+
23
+// Option signature for specifying options, e.g. WithRoundTripper.
24
+type Option func(t *Transport)
25
+
26
+// WithRoundTripper specifies the http.RoundTripper to call
27
+// next after this transport. If it is nil (default), the
28
+// transport will use http.DefaultTransport.
29
+func WithRoundTripper(rt http.RoundTripper) Option {
30
+	return func(t *Transport) {
31
+		t.rt = rt
32
+	}
33
+}
34
+
35
+// WithDefaultAttributes specifies default attributes to add
36
+// to each span.
37
+func WithDefaultAttributes(attrs ...trace.Attribute) Option {
38
+	return func(t *Transport) {
39
+		t.defaultAttributes = attrs
40
+	}
41
+}
42
+
43
+// NewTransport specifies a transport that will trace Elastic
44
+// and report back via OpenTracing.
45
+func NewTransport(opts ...Option) *Transport {
46
+	t := &Transport{}
47
+	for _, o := range opts {
48
+		o(t)
49
+	}
50
+	return t
51
+}
52
+
53
+// RoundTrip captures the request and starts an OpenTracing span
54
+// for Elastic PerformRequest operation.
55
+func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
56
+	_, span := trace.StartSpan(req.Context(), "elastic:PerformRequest")
57
+	attrs := append([]trace.Attribute(nil), t.defaultAttributes...)
58
+	attrs = append(attrs,
59
+		trace.StringAttribute("Component", "github.com/olivere/elastic/v6"),
60
+		trace.StringAttribute("Method", req.Method),
61
+		trace.StringAttribute("URL", req.URL.String()),
62
+		trace.StringAttribute("Hostname", req.URL.Hostname()),
63
+		trace.Int64Attribute("Port", atoi64(req.URL.Port())),
64
+	)
65
+	span.AddAttributes(attrs...)
66
+
67
+	var (
68
+		resp *http.Response
69
+		err  error
70
+	)
71
+	defer func() {
72
+		setSpanStatus(span, err)
73
+		span.End()
74
+	}()
75
+	if t.rt != nil {
76
+		resp, err = t.rt.RoundTrip(req)
77
+	} else {
78
+		resp, err = http.DefaultTransport.RoundTrip(req)
79
+	}
80
+	return resp, err
81
+}
82
+
83
+// See https://github.com/opencensus-integrations/ocsql/blob/master/driver.go#L749
84
+func setSpanStatus(span *trace.Span, err error) {
85
+	var status trace.Status
86
+	switch {
87
+	case err == nil:
88
+		status.Code = trace.StatusCodeOK
89
+		span.SetStatus(status)
90
+		return
91
+	case err == context.Canceled:
92
+		status.Code = trace.StatusCodeCancelled
93
+	case err == context.DeadlineExceeded:
94
+		status.Code = trace.StatusCodeDeadlineExceeded
95
+	case isConnErr(err):
96
+		status.Code = trace.StatusCodeUnavailable
97
+	case isNotFound(err):
98
+		status.Code = trace.StatusCodeNotFound
99
+	case isConflict(err):
100
+		status.Code = trace.StatusCodeFailedPrecondition
101
+	case isForbidden(err):
102
+		status.Code = trace.StatusCodePermissionDenied
103
+	case isTimeout(err):
104
+		status.Code = trace.StatusCodeResourceExhausted
105
+	default:
106
+		status.Code = trace.StatusCodeUnknown
107
+	}
108
+	status.Message = err.Error()
109
+	span.SetStatus(status)
110
+}
111
+
112
+// Copied from elastic to prevent cyclic dependencies.
113
+type elasticError struct {
114
+	Status  int           `json:"status"`
115
+	Details *errorDetails `json:"error,omitempty"`
116
+}
117
+
118
+// errorDetails encapsulate error details from Elasticsearch.
119
+// It is used in e.g. elastic.Error and elastic.BulkResponseItem.
120
+type errorDetails struct {
121
+	Type         string                   `json:"type"`
122
+	Reason       string                   `json:"reason"`
123
+	ResourceType string                   `json:"resource.type,omitempty"`
124
+	ResourceId   string                   `json:"resource.id,omitempty"`
125
+	Index        string                   `json:"index,omitempty"`
126
+	Phase        string                   `json:"phase,omitempty"`
127
+	Grouped      bool                     `json:"grouped,omitempty"`
128
+	CausedBy     map[string]interface{}   `json:"caused_by,omitempty"`
129
+	RootCause    []*errorDetails          `json:"root_cause,omitempty"`
130
+	FailedShards []map[string]interface{} `json:"failed_shards,omitempty"`
131
+}
132
+
133
+// Error returns a string representation of the error.
134
+func (e *elasticError) Error() string {
135
+	if e.Details != nil && e.Details.Reason != "" {
136
+		return fmt.Sprintf("elastic: Error %d (%s): %s [type=%s]", e.Status, http.StatusText(e.Status), e.Details.Reason, e.Details.Type)
137
+	}
138
+	return fmt.Sprintf("elastic: Error %d (%s)", e.Status, http.StatusText(e.Status))
139
+}
140
+
141
+// isContextErr returns true if the error is from a context that was canceled or deadline exceeded
142
+func isContextErr(err error) bool {
143
+	if err == context.Canceled || err == context.DeadlineExceeded {
144
+		return true
145
+	}
146
+	// This happens e.g. on redirect errors, see https://golang.org/src/net/http/client_test.go#L329
147
+	if ue, ok := err.(*url.Error); ok {
148
+		if ue.Temporary() {
149
+			return true
150
+		}
151
+		// Use of an AWS Signing Transport can result in a wrapped url.Error
152
+		return isContextErr(ue.Err)
153
+	}
154
+	return false
155
+}
156
+
157
+// isConnErr returns true if the error indicates that Elastic could not
158
+// find an Elasticsearch host to connect to.
159
+func isConnErr(err error) bool {
160
+	if err == nil {
161
+		return false
162
+	}
163
+	if err.Error() == "no Elasticsearch node available" {
164
+		return true
165
+	}
166
+	innerErr := errors.Cause(err)
167
+	if innerErr == nil {
168
+		return false
169
+	}
170
+	if innerErr.Error() == "no Elasticsearch node available" {
171
+		return true
172
+	}
173
+	return false
174
+}
175
+
176
+// isNotFound returns true if the given error indicates that Elasticsearch
177
+// returned HTTP status 404. The err parameter can be of type *elastic.Error,
178
+// elastic.Error, *http.Response or int (indicating the HTTP status code).
179
+func isNotFound(err interface{}) bool {
180
+	return isStatusCode(err, http.StatusNotFound)
181
+}
182
+
183
+// isTimeout returns true if the given error indicates that Elasticsearch
184
+// returned HTTP status 408. The err parameter can be of type *elastic.Error,
185
+// elastic.Error, *http.Response or int (indicating the HTTP status code).
186
+func isTimeout(err interface{}) bool {
187
+	return isStatusCode(err, http.StatusRequestTimeout)
188
+}
189
+
190
+// isConflict returns true if the given error indicates that the Elasticsearch
191
+// operation resulted in a version conflict. This can occur in operations like
192
+// `update` or `index` with `op_type=create`. The err parameter can be of
193
+// type *elastic.Error, elastic.Error, *http.Response or int (indicating the
194
+// HTTP status code).
195
+func isConflict(err interface{}) bool {
196
+	return isStatusCode(err, http.StatusConflict)
197
+}
198
+
199
+// isForbidden returns true if the given error indicates that Elasticsearch
200
+// returned HTTP status 403. This happens e.g. due to a missing license.
201
+// The err parameter can be of type *elastic.Error, elastic.Error,
202
+// *http.Response or int (indicating the HTTP status code).
203
+func isForbidden(err interface{}) bool {
204
+	return isStatusCode(err, http.StatusForbidden)
205
+}
206
+
207
+// isStatusCode returns true if the given error indicates that the Elasticsearch
208
+// operation returned the specified HTTP status code. The err parameter can be of
209
+// type *http.Response, *Error, Error, or int (indicating the HTTP status code).
210
+func isStatusCode(err interface{}, code int) bool {
211
+	switch e := err.(type) {
212
+	case *http.Response:
213
+		return e.StatusCode == code
214
+	case *elasticError:
215
+		return e.Status == code
216
+	case elasticError:
217
+		return e.Status == code
218
+	case int:
219
+		return e == code
220
+	}
221
+	return false
222
+}

+ 131
- 0
trace/opencensus/transport_test.go View File

@@ -0,0 +1,131 @@
1
+// Copyright 2012-present Oliver Eilhard. All rights reserved.
2
+// Use of this source code is governed by a MIT-license.
3
+// See http://olivere.mit-license.org/license.txt for details.
4
+
5
+package opencensus
6
+
7
+import (
8
+	"context"
9
+	"fmt"
10
+	"net/http"
11
+	"net/http/httptest"
12
+	"testing"
13
+
14
+	"go.opencensus.io/trace"
15
+
16
+	"github.com/olivere/elastic"
17
+)
18
+
19
+func init() {
20
+	// Always sample
21
+	trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
22
+}
23
+
24
+type testExporter struct {
25
+	spans []*trace.SpanData
26
+}
27
+
28
+func (t *testExporter) ExportSpan(s *trace.SpanData) {
29
+	t.spans = append(t.spans, s)
30
+}
31
+
32
+func TestTransport(t *testing.T) {
33
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34
+		switch r.URL.Path {
35
+		case "/":
36
+			w.WriteHeader(http.StatusOK)
37
+			fmt.Fprintln(w, `{
38
+				"name" : "Qg28M36",
39
+				"cluster_name" : "docker-cluster",
40
+				"cluster_uuid" : "rwHa7BBnRC2h8KoDfCbmuQ",
41
+				"version" : {
42
+					"number" : "6.3.2",
43
+					"build_flavor" : "oss",
44
+					"build_type" : "tar",
45
+					"build_hash" : "053779d",
46
+					"build_date" : "2018-07-20T05:20:23.451332Z",
47
+					"build_snapshot" : false,
48
+					"lucene_version" : "7.3.1",
49
+					"minimum_wire_compatibility_version" : "5.6.0",
50
+					"minimum_index_compatibility_version" : "5.0.0"
51
+				},
52
+				"tagline" : "You Know, for Search"
53
+			}`)
54
+			return
55
+		default:
56
+			w.WriteHeader(http.StatusInternalServerError)
57
+			return
58
+		}
59
+	}))
60
+	defer ts.Close()
61
+
62
+	// Register test exporter
63
+	var te testExporter
64
+	trace.RegisterExporter(&te)
65
+
66
+	// Setup a simple transport
67
+	tr := NewTransport(
68
+		WithDefaultAttributes(
69
+			trace.StringAttribute("Opaque-Id", "12345"),
70
+		),
71
+	)
72
+	httpClient := &http.Client{
73
+		Transport: tr,
74
+	}
75
+
76
+	// Create a simple Ping request via Elastic
77
+	client, err := elastic.NewClient(
78
+		elastic.SetURL(ts.URL),
79
+		elastic.SetHttpClient(httpClient),
80
+		elastic.SetHealthcheck(false),
81
+		elastic.SetSniff(false),
82
+	)
83
+	if err != nil {
84
+		t.Fatal(err)
85
+	}
86
+	res, code, err := client.Ping(ts.URL).Do(context.Background())
87
+	if err != nil {
88
+		t.Fatal(err)
89
+	}
90
+	if want, have := http.StatusOK, code; want != have {
91
+		t.Fatalf("want Status=%d, have %d", want, have)
92
+	}
93
+	if want, have := "You Know, for Search", res.TagLine; want != have {
94
+		t.Fatalf("want TagLine=%q, have %q", want, have)
95
+	}
96
+	trace.UnregisterExporter(&te)
97
+
98
+	// Check the data written into tracer
99
+	spans := te.spans
100
+	if want, have := 1, len(spans); want != have {
101
+		t.Fatalf("want %d finished spans, have %d", want, have)
102
+	}
103
+	span := spans[0]
104
+	if want, have := "elastic:PerformRequest", span.Name; want != have {
105
+		t.Fatalf("want Span.Name=%q, have %q", want, have)
106
+	}
107
+	if attr, ok := span.Attributes["Component"].(string); !ok {
108
+		t.Fatalf("attribute %q not found", "Component")
109
+	} else if want, have := "github.com/olivere/elastic/v6", attr; want != have {
110
+		t.Fatalf("want attribute=%q, have %q", want, have)
111
+	}
112
+	if attr, ok := span.Attributes["Method"].(string); !ok {
113
+		t.Fatalf("attribute %q not found", "Method")
114
+	} else if want, have := "GET", attr; want != have {
115
+		t.Fatalf("want attribute=%q, have %q", want, have)
116
+	}
117
+	if attr, ok := span.Attributes["URL"].(string); !ok || attr == "" {
118
+		t.Fatalf("attribute %q not found", "URL")
119
+	}
120
+	if attr, ok := span.Attributes["Hostname"].(string); !ok || attr == "" {
121
+		t.Fatalf("attribute %q not found", "Hostname")
122
+	}
123
+	if port, ok := span.Attributes["Port"].(int64); !ok || port <= 0 {
124
+		t.Fatalf("attribute %q not found", "Port")
125
+	}
126
+	if attr, ok := span.Attributes["Opaque-Id"].(string); !ok {
127
+		t.Fatalf("attribute %q not found", "Opaque-Id")
128
+	} else if want, have := "12345", attr; want != have {
129
+		t.Fatalf("want attribute=%q, have %q", want, have)
130
+	}
131
+}

+ 14
- 0
trace/opencensus/util.go View File

@@ -0,0 +1,14 @@
1
+// Copyright 2012-present Oliver Eilhard. All rights reserved.
2
+// Use of this source code is governed by a MIT-license.
3
+// See http://olivere.mit-license.org/license.txt for details.
4
+
5
+package opencensus
6
+
7
+import (
8
+	"strconv"
9
+)
10
+
11
+func atoi64(s string) int64 {
12
+	i, _ := strconv.ParseInt(s, 10, 64)
13
+	return i
14
+}

Loading…
Cancel
Save