tracecontext.go (239 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 apm // import "go.elastic.co/apm/v2" import ( "bytes" "encoding/hex" "fmt" "regexp" "strconv" "strings" "unicode" "github.com/pkg/errors" ) const ( elasticTracestateVendorKey = "es" ) var ( errZeroTraceID = errors.New("zero trace-id is invalid") errZeroSpanID = errors.New("zero span-id is invalid") ) // tracestateKeyRegexp holds a regular expression used for validating // tracestate keys according to the standard rules: // // key = lcalpha 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) // key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) // lcalpha = %x61-7A ; a-z // // nblkchr is used for defining valid runes for tracestate values. var ( tracestateKeyRegexp = regexp.MustCompile(`^[a-z](([a-z0-9_*/-]{0,255})|([a-z0-9_*/-]{0,240}@[a-z][a-z0-9_*/-]{0,13}))$`) nblkchr = &unicode.RangeTable{ R16: []unicode.Range16{ {0x21, 0x2B, 1}, {0x2D, 0x3C, 1}, {0x3E, 0x7E, 1}, }, LatinOffset: 3, } ) const ( traceOptionsRecordedFlag = 0x01 ) // TraceContext holds trace context for an incoming or outgoing request. type TraceContext struct { // Trace identifies the trace forest. Trace TraceID // Span identifies a span: the parent span if this context // corresponds to an incoming request, or the current span // if this is an outgoing request. Span SpanID // Options holds the trace options propagated by the parent. Options TraceOptions // State holds the trace state. State TraceState } // TraceID identifies a trace forest. type TraceID [16]byte // Validate validates the trace ID. // This will return non-nil for a zero trace ID. func (id TraceID) Validate() error { if id.isZero() { return errZeroTraceID } return nil } func (id TraceID) isZero() bool { return id == (TraceID{}) } // String returns id encoded as hex. func (id TraceID) String() string { text, _ := id.MarshalText() return string(text) } // MarshalText returns id encoded as hex, satisfying encoding.TextMarshaler. func (id TraceID) MarshalText() ([]byte, error) { text := make([]byte, hex.EncodedLen(len(id))) hex.Encode(text, id[:]) return text, nil } // SpanID identifies a span within a trace. type SpanID [8]byte // Validate validates the span ID. // This will return non-nil for a zero span ID. func (id SpanID) Validate() error { if id.isZero() { return errZeroSpanID } return nil } func (id SpanID) isZero() bool { return id == SpanID{} } // String returns id encoded as hex. func (id SpanID) String() string { text, _ := id.MarshalText() return string(text) } // MarshalText returns id encoded as hex, satisfying encoding.TextMarshaler. func (id SpanID) MarshalText() ([]byte, error) { text := make([]byte, hex.EncodedLen(len(id))) hex.Encode(text, id[:]) return text, nil } // SpanLink describes a linked span. type SpanLink struct { Trace TraceID Span SpanID } // TraceOptions describes the options for a trace. type TraceOptions uint8 // Recorded reports whether or not the transaction/span may have been (or may be) recorded. func (o TraceOptions) Recorded() bool { return (o & traceOptionsRecordedFlag) == traceOptionsRecordedFlag } // WithRecorded changes the "recorded" flag, and returns the new options // without modifying the original value. func (o TraceOptions) WithRecorded(recorded bool) TraceOptions { if recorded { return o | traceOptionsRecordedFlag } return o & (0xFF ^ traceOptionsRecordedFlag) } // TraceState holds vendor-specific state for a trace. type TraceState struct { head *TraceStateEntry // Fields related to parsing the Elastic ("es") tracestate entry. // // These must not be modified after NewTraceState returns. parseElasticTracestateError error haveSampleRate bool haveElastic bool sampleRate float64 } // NewTraceState returns a TraceState based on entries. func NewTraceState(entries ...TraceStateEntry) TraceState { var out TraceState var last *TraceStateEntry var haveElastic bool for _, e := range entries { if e.Key == elasticTracestateVendorKey { if haveElastic { // Discard duplicate `es` entries; keep the last entry's value. out.head.Value = e.Value continue } haveElastic = true e := e // copy e.next = out.head // move the current head reference to `es`.next. out.head = &e // swap the head with the current `es` entry. // To preserve the previous entries in the linked list, set the // `last` reference to the current key only when `last` is empty. if last == nil { last = &e } continue } e := e // copy if last == nil { out.head = &e } else { last.next = &e } last = &e } if haveElastic { out.parseElasticTracestateError = out.parseElasticTracestate(*out.head) out.haveElastic = true } return out } // parseElasticTracestate parses an Elastic ("es") tracestate entry. // // Per https://github.com/elastic/apm/blob/main/specs/agents/tracing-distributed-tracing.md, // the "es" tracestate value format is: "key:value;key:value...". Unknown keys are ignored. func (s *TraceState) parseElasticTracestate(e TraceStateEntry) error { if err := e.Validate(); err != nil { return err } value := e.Value for value != "" { kv := value end := strings.IndexRune(value, ';') if end >= 0 { kv = value[:end] value = value[end+1:] } else { value = "" } sep := strings.IndexRune(kv, ':') if sep == -1 { return errors.New("malformed 'es' tracestate entry") } k, v := kv[:sep], kv[sep+1:] switch k { case "s": sampleRate, err := strconv.ParseFloat(v, 64) if err != nil { return err } if sampleRate < 0 || sampleRate > 1 { return fmt.Errorf("sample rate %q out of range", v) } s.sampleRate = sampleRate s.haveSampleRate = true } } return nil } // String returns s as a comma-separated list of key-value pairs. func (s TraceState) String() string { if s.head == nil { return "" } var buf bytes.Buffer s.head.writeBuf(&buf) for e := s.head.next; e != nil; e = e.next { buf.WriteByte(',') e.writeBuf(&buf) } return buf.String() } // Validate validates the trace state. // // This will return non-nil if any entries are invalid or // if there are too many entries. func (s TraceState) Validate() error { if s.head == nil { return nil } var i int for e := s.head; e != nil; e = e.next { if i == 32 { return errors.New("tracestate contains more than the maximum allowed number of entries, 32") } if e.Key == elasticTracestateVendorKey { // s.parseElasticTracestateError holds a general e.Validate error if any // occurred, or any other error specific to the Elastic tracestate format. if err := s.parseElasticTracestateError; err != nil { return errors.Wrapf(err, "invalid tracestate entry at position %d", i) } } else { if err := e.Validate(); err != nil { return errors.Wrapf(err, "invalid tracestate entry at position %d", i) } } i++ } return nil } // TraceStateEntry holds a trace state entry: a key/value pair // representing state for a vendor. type TraceStateEntry struct { next *TraceStateEntry // Key holds a vendor (and optionally, tenant) ID. Key string // Value holds a string representing trace state. Value string } func (e *TraceStateEntry) writeBuf(buf *bytes.Buffer) { buf.WriteString(e.Key) buf.WriteByte('=') buf.WriteString(e.Value) } // Validate validates the trace state entry. // // This will return non-nil if either the key or value is invalid. func (e *TraceStateEntry) Validate() error { if e.Key != elasticTracestateVendorKey && !tracestateKeyRegexp.MatchString(e.Key) { return fmt.Errorf("invalid key %q", e.Key) } if err := e.validateValue(); err != nil { return errors.Wrapf(err, "invalid value for key %q", e.Key) } return nil } func (e *TraceStateEntry) validateValue() error { if e.Value == "" { return errors.New("value is empty") } runes := []rune(e.Value) n := len(runes) if n > 256 { return errors.Errorf("value contains %d characters, maximum allowed is 256", n) } if !unicode.In(runes[n-1], nblkchr) { return errors.Errorf("value contains invalid character %q", runes[n-1]) } for _, r := range runes[:n-1] { if r != 0x20 && !unicode.In(r, nblkchr) { return errors.Errorf("value contains invalid character %q", r) } } return nil } func formatElasticTracestateValue(sampleRate float64) string { // 0 -> "s:0" // 1 -> "s:1" // 0.55555 -> "s:0.5555" (any rounding should be applied prior) return "s:" + strconv.FormatFloat(sampleRate, 'g', 4, 64) }