1 // Copyright 2018, OpenCensus Authors
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
22 "go.opencensus.io/plugin/ochttp/propagation/b3"
23 "go.opencensus.io/trace"
24 "go.opencensus.io/trace/propagation"
27 // TODO(jbd): Add godoc examples.
29 var defaultFormat propagation.HTTPFormat = &b3.HTTPFormat{}
31 // Attributes recorded on the span for the requests.
32 // Only trace exporters will need them.
34 HostAttribute = "http.host"
35 MethodAttribute = "http.method"
36 PathAttribute = "http.path"
37 URLAttribute = "http.url"
38 UserAgentAttribute = "http.user_agent"
39 StatusCodeAttribute = "http.status_code"
42 type traceTransport struct {
43 base http.RoundTripper
44 startOptions trace.StartOptions
45 format propagation.HTTPFormat
46 formatSpanName func(*http.Request) string
47 newClientTrace func(*http.Request, *trace.Span) *httptrace.ClientTrace
50 // TODO(jbd): Add message events for request and response size.
52 // RoundTrip creates a trace.Span and inserts it into the outgoing request's headers.
53 // The created span can follow a parent span, if a parent is presented in
54 // the request's context.
55 func (t *traceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
56 name := t.formatSpanName(req)
57 // TODO(jbd): Discuss whether we want to prefix
58 // outgoing requests with Sent.
59 ctx, span := trace.StartSpan(req.Context(), name,
60 trace.WithSampler(t.startOptions.Sampler),
61 trace.WithSpanKind(trace.SpanKindClient))
63 if t.newClientTrace != nil {
64 req = req.WithContext(httptrace.WithClientTrace(ctx, t.newClientTrace(req, span)))
66 req = req.WithContext(ctx)
70 // SpanContextToRequest will modify its Request argument, which is
71 // contrary to the contract for http.RoundTripper, so we need to
72 // pass it a copy of the Request.
73 // However, the Request struct itself was already copied by
74 // the WithContext calls above and so we just need to copy the header.
75 header := make(http.Header)
76 for k, v := range req.Header {
80 t.format.SpanContextToRequest(span.SpanContext(), req)
83 span.AddAttributes(requestAttrs(req)...)
84 resp, err := t.base.RoundTrip(req)
86 span.SetStatus(trace.Status{Code: trace.StatusCodeUnknown, Message: err.Error()})
91 span.AddAttributes(responseAttrs(resp)...)
92 span.SetStatus(TraceStatus(resp.StatusCode, resp.Status))
94 // span.End() will be invoked after
95 // a read from resp.Body returns io.EOF or when
96 // resp.Body.Close() is invoked.
97 bt := &bodyTracker{rc: resp.Body, span: span}
98 resp.Body = wrappedBody(bt, resp.Body)
102 // bodyTracker wraps a response.Body and invokes
103 // trace.EndSpan on encountering io.EOF on reading
104 // the body of the original response.
105 type bodyTracker struct {
110 var _ io.ReadCloser = (*bodyTracker)(nil)
112 func (bt *bodyTracker) Read(b []byte) (int, error) {
113 n, err := bt.rc.Read(b)
121 // For all other errors, set the span status
122 bt.span.SetStatus(trace.Status{
123 // Code 2 is the error code for Internal server error.
125 Message: err.Error(),
131 func (bt *bodyTracker) Close() error {
132 // Invoking endSpan on Close will help catch the cases
133 // in which a read returned a non-nil error, we set the
134 // span status but didn't end the span.
139 // CancelRequest cancels an in-flight request by closing its connection.
140 func (t *traceTransport) CancelRequest(req *http.Request) {
141 type canceler interface {
142 CancelRequest(*http.Request)
144 if cr, ok := t.base.(canceler); ok {
145 cr.CancelRequest(req)
149 func spanNameFromURL(req *http.Request) string {
153 func requestAttrs(r *http.Request) []trace.Attribute {
154 userAgent := r.UserAgent()
156 attrs := make([]trace.Attribute, 0, 5)
157 attrs = append(attrs,
158 trace.StringAttribute(PathAttribute, r.URL.Path),
159 trace.StringAttribute(URLAttribute, r.URL.String()),
160 trace.StringAttribute(HostAttribute, r.Host),
161 trace.StringAttribute(MethodAttribute, r.Method),
165 attrs = append(attrs, trace.StringAttribute(UserAgentAttribute, userAgent))
171 func responseAttrs(resp *http.Response) []trace.Attribute {
172 return []trace.Attribute{
173 trace.Int64Attribute(StatusCodeAttribute, int64(resp.StatusCode)),
177 // TraceStatus is a utility to convert the HTTP status code to a trace.Status that
178 // represents the outcome as closely as possible.
179 func TraceStatus(httpStatusCode int, statusLine string) trace.Status {
181 if httpStatusCode < 200 || httpStatusCode >= 400 {
182 code = trace.StatusCodeUnknown
184 switch httpStatusCode {
186 code = trace.StatusCodeCancelled
187 case http.StatusBadRequest:
188 code = trace.StatusCodeInvalidArgument
189 case http.StatusGatewayTimeout:
190 code = trace.StatusCodeDeadlineExceeded
191 case http.StatusNotFound:
192 code = trace.StatusCodeNotFound
193 case http.StatusForbidden:
194 code = trace.StatusCodePermissionDenied
195 case http.StatusUnauthorized: // 401 is actually unauthenticated.
196 code = trace.StatusCodeUnauthenticated
197 case http.StatusTooManyRequests:
198 code = trace.StatusCodeResourceExhausted
199 case http.StatusNotImplemented:
200 code = trace.StatusCodeUnimplemented
201 case http.StatusServiceUnavailable:
202 code = trace.StatusCodeUnavailable
204 code = trace.StatusCodeOK
206 return trace.Status{Code: code, Message: codeToStr[code]}
209 var codeToStr = map[int32]string{
210 trace.StatusCodeOK: `OK`,
211 trace.StatusCodeCancelled: `CANCELLED`,
212 trace.StatusCodeUnknown: `UNKNOWN`,
213 trace.StatusCodeInvalidArgument: `INVALID_ARGUMENT`,
214 trace.StatusCodeDeadlineExceeded: `DEADLINE_EXCEEDED`,
215 trace.StatusCodeNotFound: `NOT_FOUND`,
216 trace.StatusCodeAlreadyExists: `ALREADY_EXISTS`,
217 trace.StatusCodePermissionDenied: `PERMISSION_DENIED`,
218 trace.StatusCodeResourceExhausted: `RESOURCE_EXHAUSTED`,
219 trace.StatusCodeFailedPrecondition: `FAILED_PRECONDITION`,
220 trace.StatusCodeAborted: `ABORTED`,
221 trace.StatusCodeOutOfRange: `OUT_OF_RANGE`,
222 trace.StatusCodeUnimplemented: `UNIMPLEMENTED`,
223 trace.StatusCodeInternal: `INTERNAL`,
224 trace.StatusCodeUnavailable: `UNAVAILABLE`,
225 trace.StatusCodeDataLoss: `DATA_LOSS`,
226 trace.StatusCodeUnauthenticated: `UNAUTHENTICATED`,
229 func isHealthEndpoint(path string) bool {
230 // Health checking is pretty frequent and
231 // traces collected for health endpoints
232 // can be extremely noisy and expensive.
233 // Disable canonical health checking endpoints
234 // like /healthz and /_ah/health for now.
235 if path == "/healthz" || path == "/_ah/health" {