| // Copyright 2018, OpenCensus Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Package tracecontext contains HTTP propagator for TraceContext standard. |
| // See https://github.com/w3c/distributed-tracing for more information. |
| package tracecontext // import "go.opencensus.io/plugin/ochttp/propagation/tracecontext" |
| |
| import ( |
| "encoding/hex" |
| "fmt" |
| "net/http" |
| "net/textproto" |
| "regexp" |
| "strings" |
| |
| "go.opencensus.io/trace" |
| "go.opencensus.io/trace/propagation" |
| "go.opencensus.io/trace/tracestate" |
| ) |
| |
| const ( |
| supportedVersion = 0 |
| maxVersion = 254 |
| maxTracestateLen = 512 |
| traceparentHeader = "traceparent" |
| tracestateHeader = "tracestate" |
| trimOWSRegexFmt = `^[\x09\x20]*(.*[^\x20\x09])[\x09\x20]*$` |
| ) |
| |
| var trimOWSRegExp = regexp.MustCompile(trimOWSRegexFmt) |
| |
| var _ propagation.HTTPFormat = (*HTTPFormat)(nil) |
| |
| // HTTPFormat implements the TraceContext trace propagation format. |
| type HTTPFormat struct{} |
| |
| // SpanContextFromRequest extracts a span context from incoming requests. |
| func (f *HTTPFormat) SpanContextFromRequest(req *http.Request) (sc trace.SpanContext, ok bool) { |
| h, ok := getRequestHeader(req, traceparentHeader, false) |
| if !ok { |
| return trace.SpanContext{}, false |
| } |
| sections := strings.Split(h, "-") |
| if len(sections) < 4 { |
| return trace.SpanContext{}, false |
| } |
| |
| if len(sections[0]) != 2 { |
| return trace.SpanContext{}, false |
| } |
| ver, err := hex.DecodeString(sections[0]) |
| if err != nil { |
| return trace.SpanContext{}, false |
| } |
| version := int(ver[0]) |
| if version > maxVersion { |
| return trace.SpanContext{}, false |
| } |
| |
| if version == 0 && len(sections) != 4 { |
| return trace.SpanContext{}, false |
| } |
| |
| if len(sections[1]) != 32 { |
| return trace.SpanContext{}, false |
| } |
| tid, err := hex.DecodeString(sections[1]) |
| if err != nil { |
| return trace.SpanContext{}, false |
| } |
| copy(sc.TraceID[:], tid) |
| |
| if len(sections[2]) != 16 { |
| return trace.SpanContext{}, false |
| } |
| sid, err := hex.DecodeString(sections[2]) |
| if err != nil { |
| return trace.SpanContext{}, false |
| } |
| copy(sc.SpanID[:], sid) |
| |
| opts, err := hex.DecodeString(sections[3]) |
| if err != nil || len(opts) < 1 { |
| return trace.SpanContext{}, false |
| } |
| sc.TraceOptions = trace.TraceOptions(opts[0]) |
| |
| // Don't allow all zero trace or span ID. |
| if sc.TraceID == [16]byte{} || sc.SpanID == [8]byte{} { |
| return trace.SpanContext{}, false |
| } |
| |
| sc.Tracestate = tracestateFromRequest(req) |
| return sc, true |
| } |
| |
| // getRequestHeader returns a combined header field according to RFC7230 section 3.2.2. |
| // If commaSeparated is true, multiple header fields with the same field name using be |
| // combined using ",". |
| // If no header was found using the given name, "ok" would be false. |
| // If more than one headers was found using the given name, while commaSeparated is false, |
| // "ok" would be false. |
| func getRequestHeader(req *http.Request, name string, commaSeparated bool) (hdr string, ok bool) { |
| v := req.Header[textproto.CanonicalMIMEHeaderKey(name)] |
| switch len(v) { |
| case 0: |
| return "", false |
| case 1: |
| return v[0], true |
| default: |
| return strings.Join(v, ","), commaSeparated |
| } |
| } |
| |
| // TODO(rghetia): return an empty Tracestate when parsing tracestate header encounters an error. |
| // Revisit to return additional boolean value to indicate parsing error when following issues |
| // are resolved. |
| // https://github.com/w3c/distributed-tracing/issues/172 |
| // https://github.com/w3c/distributed-tracing/issues/175 |
| func tracestateFromRequest(req *http.Request) *tracestate.Tracestate { |
| h, _ := getRequestHeader(req, tracestateHeader, true) |
| if h == "" { |
| return nil |
| } |
| |
| var entries []tracestate.Entry |
| pairs := strings.Split(h, ",") |
| hdrLenWithoutOWS := len(pairs) - 1 // Number of commas |
| for _, pair := range pairs { |
| matches := trimOWSRegExp.FindStringSubmatch(pair) |
| if matches == nil { |
| return nil |
| } |
| pair = matches[1] |
| hdrLenWithoutOWS += len(pair) |
| if hdrLenWithoutOWS > maxTracestateLen { |
| return nil |
| } |
| kv := strings.Split(pair, "=") |
| if len(kv) != 2 { |
| return nil |
| } |
| entries = append(entries, tracestate.Entry{Key: kv[0], Value: kv[1]}) |
| } |
| ts, err := tracestate.New(nil, entries...) |
| if err != nil { |
| return nil |
| } |
| |
| return ts |
| } |
| |
| func tracestateToRequest(sc trace.SpanContext, req *http.Request) { |
| var pairs = make([]string, 0, len(sc.Tracestate.Entries())) |
| if sc.Tracestate != nil { |
| for _, entry := range sc.Tracestate.Entries() { |
| pairs = append(pairs, strings.Join([]string{entry.Key, entry.Value}, "=")) |
| } |
| h := strings.Join(pairs, ",") |
| |
| if h != "" && len(h) <= maxTracestateLen { |
| req.Header.Set(tracestateHeader, h) |
| } |
| } |
| } |
| |
| // SpanContextToRequest modifies the given request to include traceparent and tracestate headers. |
| func (f *HTTPFormat) SpanContextToRequest(sc trace.SpanContext, req *http.Request) { |
| h := fmt.Sprintf("%x-%x-%x-%x", |
| []byte{supportedVersion}, |
| sc.TraceID[:], |
| sc.SpanID[:], |
| []byte{byte(sc.TraceOptions)}) |
| req.Header.Set(traceparentHeader, h) |
| tracestateToRequest(sc, req) |
| } |