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  }