github.com/waldiirawan/apm-agent-go/v2@v2.2.2/tracecontext.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package apm // import "github.com/waldiirawan/apm-agent-go/v2" 19 20 import ( 21 "bytes" 22 "encoding/hex" 23 "fmt" 24 "regexp" 25 "strconv" 26 "strings" 27 "unicode" 28 29 "github.com/pkg/errors" 30 ) 31 32 const ( 33 elasticTracestateVendorKey = "es" 34 ) 35 36 var ( 37 errZeroTraceID = errors.New("zero trace-id is invalid") 38 errZeroSpanID = errors.New("zero span-id is invalid") 39 ) 40 41 // tracestateKeyRegexp holds a regular expression used for validating 42 // tracestate keys according to the standard rules: 43 // 44 // key = lcalpha 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) 45 // key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) 46 // lcalpha = %x61-7A ; a-z 47 // 48 // nblkchr is used for defining valid runes for tracestate values. 49 var ( 50 tracestateKeyRegexp = regexp.MustCompile(`^[a-z](([a-z0-9_*/-]{0,255})|([a-z0-9_*/-]{0,240}@[a-z][a-z0-9_*/-]{0,13}))$`) 51 52 nblkchr = &unicode.RangeTable{ 53 R16: []unicode.Range16{ 54 {0x21, 0x2B, 1}, 55 {0x2D, 0x3C, 1}, 56 {0x3E, 0x7E, 1}, 57 }, 58 LatinOffset: 3, 59 } 60 ) 61 62 const ( 63 traceOptionsRecordedFlag = 0x01 64 ) 65 66 // TraceContext holds trace context for an incoming or outgoing request. 67 type TraceContext struct { 68 // Trace identifies the trace forest. 69 Trace TraceID 70 71 // Span identifies a span: the parent span if this context 72 // corresponds to an incoming request, or the current span 73 // if this is an outgoing request. 74 Span SpanID 75 76 // Options holds the trace options propagated by the parent. 77 Options TraceOptions 78 79 // State holds the trace state. 80 State TraceState 81 } 82 83 // TraceID identifies a trace forest. 84 type TraceID [16]byte 85 86 // Validate validates the trace ID. 87 // This will return non-nil for a zero trace ID. 88 func (id TraceID) Validate() error { 89 if id.isZero() { 90 return errZeroTraceID 91 } 92 return nil 93 } 94 95 func (id TraceID) isZero() bool { 96 return id == (TraceID{}) 97 } 98 99 // String returns id encoded as hex. 100 func (id TraceID) String() string { 101 text, _ := id.MarshalText() 102 return string(text) 103 } 104 105 // MarshalText returns id encoded as hex, satisfying encoding.TextMarshaler. 106 func (id TraceID) MarshalText() ([]byte, error) { 107 text := make([]byte, hex.EncodedLen(len(id))) 108 hex.Encode(text, id[:]) 109 return text, nil 110 } 111 112 // SpanID identifies a span within a trace. 113 type SpanID [8]byte 114 115 // Validate validates the span ID. 116 // This will return non-nil for a zero span ID. 117 func (id SpanID) Validate() error { 118 if id.isZero() { 119 return errZeroSpanID 120 } 121 return nil 122 } 123 124 func (id SpanID) isZero() bool { 125 return id == SpanID{} 126 } 127 128 // String returns id encoded as hex. 129 func (id SpanID) String() string { 130 text, _ := id.MarshalText() 131 return string(text) 132 } 133 134 // MarshalText returns id encoded as hex, satisfying encoding.TextMarshaler. 135 func (id SpanID) MarshalText() ([]byte, error) { 136 text := make([]byte, hex.EncodedLen(len(id))) 137 hex.Encode(text, id[:]) 138 return text, nil 139 } 140 141 // SpanLink describes a linked span. 142 type SpanLink struct { 143 Trace TraceID 144 Span SpanID 145 } 146 147 // TraceOptions describes the options for a trace. 148 type TraceOptions uint8 149 150 // Recorded reports whether or not the transaction/span may have been (or may be) recorded. 151 func (o TraceOptions) Recorded() bool { 152 return (o & traceOptionsRecordedFlag) == traceOptionsRecordedFlag 153 } 154 155 // WithRecorded changes the "recorded" flag, and returns the new options 156 // without modifying the original value. 157 func (o TraceOptions) WithRecorded(recorded bool) TraceOptions { 158 if recorded { 159 return o | traceOptionsRecordedFlag 160 } 161 return o & (0xFF ^ traceOptionsRecordedFlag) 162 } 163 164 // TraceState holds vendor-specific state for a trace. 165 type TraceState struct { 166 head *TraceStateEntry 167 168 // Fields related to parsing the Elastic ("es") tracestate entry. 169 // 170 // These must not be modified after NewTraceState returns. 171 parseElasticTracestateError error 172 haveSampleRate bool 173 haveElastic bool 174 sampleRate float64 175 } 176 177 // NewTraceState returns a TraceState based on entries. 178 func NewTraceState(entries ...TraceStateEntry) TraceState { 179 var out TraceState 180 var last *TraceStateEntry 181 var haveElastic bool 182 for _, e := range entries { 183 if e.Key == elasticTracestateVendorKey { 184 if haveElastic { 185 // Discard duplicate `es` entries; keep the last entry's value. 186 out.head.Value = e.Value 187 continue 188 } 189 haveElastic = true 190 e := e // copy 191 e.next = out.head // move the current head reference to `es`.next. 192 out.head = &e // swap the head with the current `es` entry. 193 // To preserve the previous entries in the linked list, set the 194 // `last` reference to the current key only when `last` is empty. 195 if last == nil { 196 last = &e 197 } 198 continue 199 } 200 e := e // copy 201 if last == nil { 202 out.head = &e 203 } else { 204 last.next = &e 205 } 206 last = &e 207 } 208 if haveElastic { 209 out.parseElasticTracestateError = out.parseElasticTracestate(*out.head) 210 out.haveElastic = true 211 } 212 return out 213 } 214 215 // parseElasticTracestate parses an Elastic ("es") tracestate entry. 216 // 217 // Per https://github.com/elastic/apm/blob/main/specs/agents/tracing-distributed-tracing.md, 218 // the "es" tracestate value format is: "key:value;key:value...". Unknown keys are ignored. 219 func (s *TraceState) parseElasticTracestate(e TraceStateEntry) error { 220 if err := e.Validate(); err != nil { 221 return err 222 } 223 value := e.Value 224 for value != "" { 225 kv := value 226 end := strings.IndexRune(value, ';') 227 if end >= 0 { 228 kv = value[:end] 229 value = value[end+1:] 230 } else { 231 value = "" 232 } 233 sep := strings.IndexRune(kv, ':') 234 if sep == -1 { 235 return errors.New("malformed 'es' tracestate entry") 236 } 237 k, v := kv[:sep], kv[sep+1:] 238 switch k { 239 case "s": 240 sampleRate, err := strconv.ParseFloat(v, 64) 241 if err != nil { 242 return err 243 } 244 if sampleRate < 0 || sampleRate > 1 { 245 return fmt.Errorf("sample rate %q out of range", v) 246 } 247 s.sampleRate = sampleRate 248 s.haveSampleRate = true 249 } 250 } 251 return nil 252 } 253 254 // String returns s as a comma-separated list of key-value pairs. 255 func (s TraceState) String() string { 256 if s.head == nil { 257 return "" 258 } 259 var buf bytes.Buffer 260 s.head.writeBuf(&buf) 261 for e := s.head.next; e != nil; e = e.next { 262 buf.WriteByte(',') 263 e.writeBuf(&buf) 264 } 265 return buf.String() 266 } 267 268 // Validate validates the trace state. 269 // 270 // This will return non-nil if any entries are invalid or 271 // if there are too many entries. 272 func (s TraceState) Validate() error { 273 if s.head == nil { 274 return nil 275 } 276 var i int 277 for e := s.head; e != nil; e = e.next { 278 if i == 32 { 279 return errors.New("tracestate contains more than the maximum allowed number of entries, 32") 280 } 281 if e.Key == elasticTracestateVendorKey { 282 // s.parseElasticTracestateError holds a general e.Validate error if any 283 // occurred, or any other error specific to the Elastic tracestate format. 284 if err := s.parseElasticTracestateError; err != nil { 285 return errors.Wrapf(err, "invalid tracestate entry at position %d", i) 286 } 287 } else { 288 if err := e.Validate(); err != nil { 289 return errors.Wrapf(err, "invalid tracestate entry at position %d", i) 290 } 291 } 292 i++ 293 } 294 return nil 295 } 296 297 // TraceStateEntry holds a trace state entry: a key/value pair 298 // representing state for a vendor. 299 type TraceStateEntry struct { 300 next *TraceStateEntry 301 302 // Key holds a vendor (and optionally, tenant) ID. 303 Key string 304 305 // Value holds a string representing trace state. 306 Value string 307 } 308 309 func (e *TraceStateEntry) writeBuf(buf *bytes.Buffer) { 310 buf.WriteString(e.Key) 311 buf.WriteByte('=') 312 buf.WriteString(e.Value) 313 } 314 315 // Validate validates the trace state entry. 316 // 317 // This will return non-nil if either the key or value is invalid. 318 func (e *TraceStateEntry) Validate() error { 319 if e.Key != elasticTracestateVendorKey && !tracestateKeyRegexp.MatchString(e.Key) { 320 return fmt.Errorf("invalid key %q", e.Key) 321 } 322 if err := e.validateValue(); err != nil { 323 return errors.Wrapf(err, "invalid value for key %q", e.Key) 324 } 325 return nil 326 } 327 328 func (e *TraceStateEntry) validateValue() error { 329 if e.Value == "" { 330 return errors.New("value is empty") 331 } 332 runes := []rune(e.Value) 333 n := len(runes) 334 if n > 256 { 335 return errors.Errorf("value contains %d characters, maximum allowed is 256", n) 336 } 337 if !unicode.In(runes[n-1], nblkchr) { 338 return errors.Errorf("value contains invalid character %q", runes[n-1]) 339 } 340 for _, r := range runes[:n-1] { 341 if r != 0x20 && !unicode.In(r, nblkchr) { 342 return errors.Errorf("value contains invalid character %q", r) 343 } 344 } 345 return nil 346 } 347 348 func formatElasticTracestateValue(sampleRate float64) string { 349 // 0 -> "s:0" 350 // 1 -> "s:1" 351 // 0.55555 -> "s:0.5555" (any rounding should be applied prior) 352 return fmt.Sprintf("s:%.4g", sampleRate) 353 }