package jaeger import ( "errors" "fmt" "strconv" "strings" "sync" "go.uber.org/atomic" ) const ( flagSampled = 1 flagDebug = 2 flagFirehose = 8 ) var ( errEmptyTracerStateString = errors.New("Cannot convert empty string to tracer state") errMalformedTracerStateString = errors.New("String does not match tracer state format") emptyContext = SpanContext{} ) // TraceID represents unique 128bit identifier of a trace type TraceID struct { High, Low uint64 } // SpanID represents unique 64bit identifier of a span type SpanID uint64 // SpanContext represents propagated span identity and state type SpanContext struct { // traceID represents globally unique ID of the trace. // Usually generated as a random number. traceID TraceID // spanID represents span ID that must be unique within its trace, // but does not have to be globally unique. spanID SpanID // parentID refers to the ID of the parent span. // Should be 0 if the current span is a root span. parentID SpanID // Distributed Context baggage. The is a snapshot in time. baggage map[string]string // debugID can be set to some correlation ID when the context is being // extracted from a TextMap carrier. // // See JaegerDebugHeader in constants.go debugID string // samplingState is shared across all spans samplingState *samplingState // remote indicates that span context represents a remote parent remote bool } type samplingState struct { // Span context's state flags that are propagated across processes. Only lower 8 bits are used. // We use an int32 instead of byte to be able to use CAS operations. stateFlags atomic.Int32 // When state is not final, sampling will be retried on other span write operations, // like SetOperationName / SetTag, and the spans will remain writable. final atomic.Bool // localRootSpan stores the SpanID of the first span created in this process for a given trace. localRootSpan SpanID // extendedState allows samplers to keep intermediate state. // The keys and values in this map are completely opaque: interface{} -> interface{}. extendedState sync.Map } func (s *samplingState) isLocalRootSpan(id SpanID) bool { return id == s.localRootSpan } func (s *samplingState) setFlag(newFlag int32) { swapped := false for !swapped { old := s.stateFlags.Load() swapped = s.stateFlags.CAS(old, old|newFlag) } } func (s *samplingState) unsetFlag(newFlag int32) { swapped := false for !swapped { old := s.stateFlags.Load() swapped = s.stateFlags.CAS(old, old&^newFlag) } } func (s *samplingState) setSampled() { s.setFlag(flagSampled) } func (s *samplingState) unsetSampled() { s.unsetFlag(flagSampled) } func (s *samplingState) setDebugAndSampled() { s.setFlag(flagDebug | flagSampled) } func (s *samplingState) setFirehose() { s.setFlag(flagFirehose) } func (s *samplingState) setFlags(flags byte) { s.stateFlags.Store(int32(flags)) } func (s *samplingState) setFinal() { s.final.Store(true) } func (s *samplingState) flags() byte { return byte(s.stateFlags.Load()) } func (s *samplingState) isSampled() bool { return s.stateFlags.Load()&flagSampled == flagSampled } func (s *samplingState) isDebug() bool { return s.stateFlags.Load()&flagDebug == flagDebug } func (s *samplingState) isFirehose() bool { return s.stateFlags.Load()&flagFirehose == flagFirehose } func (s *samplingState) isFinal() bool { return s.final.Load() } func (s *samplingState) extendedStateForKey(key interface{}, initValue func() interface{}) interface{} { if value, ok := s.extendedState.Load(key); ok { return value } value := initValue() value, _ = s.extendedState.LoadOrStore(key, value) return value } // ForeachBaggageItem implements ForeachBaggageItem() of opentracing.SpanContext func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) { for k, v := range c.baggage { if !handler(k, v) { break } } } // IsSampled returns whether this trace was chosen for permanent storage // by the sampling mechanism of the tracer. func (c SpanContext) IsSampled() bool { return c.samplingState.isSampled() } // IsDebug indicates whether sampling was explicitly requested by the service. func (c SpanContext) IsDebug() bool { return c.samplingState.isDebug() } // IsSamplingFinalized indicates whether the sampling decision has been finalized. func (c SpanContext) IsSamplingFinalized() bool { return c.samplingState.isFinal() } // IsFirehose indicates whether the firehose flag was set func (c SpanContext) IsFirehose() bool { return c.samplingState.isFirehose() } // ExtendedSamplingState returns the custom state object for a given key. If the value for this key does not exist, // it is initialized via initValue function. This state can be used by samplers (e.g. x.PrioritySampler). func (c SpanContext) ExtendedSamplingState(key interface{}, initValue func() interface{}) interface{} { return c.samplingState.extendedStateForKey(key, initValue) } // IsValid indicates whether this context actually represents a valid trace. func (c SpanContext) IsValid() bool { return c.traceID.IsValid() && c.spanID != 0 } // SetFirehose enables firehose mode for this trace. func (c SpanContext) SetFirehose() { c.samplingState.setFirehose() } func (c SpanContext) String() string { if c.traceID.High == 0 { return fmt.Sprintf("%016x:%016x:%016x:%x", c.traceID.Low, uint64(c.spanID), uint64(c.parentID), c.samplingState.stateFlags.Load()) } return fmt.Sprintf("%016x%016x:%016x:%016x:%x", c.traceID.High, c.traceID.Low, uint64(c.spanID), uint64(c.parentID), c.samplingState.stateFlags.Load()) } // ContextFromString reconstructs the Context encoded in a string func ContextFromString(value string) (SpanContext, error) { var context SpanContext if value == "" { return emptyContext, errEmptyTracerStateString } parts := strings.Split(value, ":") if len(parts) != 4 { return emptyContext, errMalformedTracerStateString } var err error if context.traceID, err = TraceIDFromString(parts[0]); err != nil { return emptyContext, err } if context.spanID, err = SpanIDFromString(parts[1]); err != nil { return emptyContext, err } if context.parentID, err = SpanIDFromString(parts[2]); err != nil { return emptyContext, err } flags, err := strconv.ParseUint(parts[3], 10, 8) if err != nil { return emptyContext, err } context.samplingState = &samplingState{} context.samplingState.setFlags(byte(flags)) return context, nil } // TraceID returns the trace ID of this span context func (c SpanContext) TraceID() TraceID { return c.traceID } // SpanID returns the span ID of this span context func (c SpanContext) SpanID() SpanID { return c.spanID } // ParentID returns the parent span ID of this span context func (c SpanContext) ParentID() SpanID { return c.parentID } // Flags returns the bitmap containing such bits as 'sampled' and 'debug'. func (c SpanContext) Flags() byte { return c.samplingState.flags() } // NewSpanContext creates a new instance of SpanContext func NewSpanContext(traceID TraceID, spanID, parentID SpanID, sampled bool, baggage map[string]string) SpanContext { samplingState := &samplingState{} if sampled { samplingState.setSampled() } return SpanContext{ traceID: traceID, spanID: spanID, parentID: parentID, samplingState: samplingState, baggage: baggage} } // CopyFrom copies data from ctx into this context, including span identity and baggage. // TODO This is only used by interop.go. Remove once TChannel Go supports OpenTracing. func (c *SpanContext) CopyFrom(ctx *SpanContext) { c.traceID = ctx.traceID c.spanID = ctx.spanID c.parentID = ctx.parentID c.samplingState = ctx.samplingState if l := len(ctx.baggage); l > 0 { c.baggage = make(map[string]string, l) for k, v := range ctx.baggage { c.baggage[k] = v } } else { c.baggage = nil } } // WithBaggageItem creates a new context with an extra baggage item. func (c SpanContext) WithBaggageItem(key, value string) SpanContext { var newBaggage map[string]string if c.baggage == nil { newBaggage = map[string]string{key: value} } else { newBaggage = make(map[string]string, len(c.baggage)+1) for k, v := range c.baggage { newBaggage[k] = v } newBaggage[key] = value } // Use positional parameters so the compiler will help catch new fields. return SpanContext{c.traceID, c.spanID, c.parentID, newBaggage, "", c.samplingState, c.remote} } // isDebugIDContainerOnly returns true when the instance of the context is only // used to return the debug/correlation ID from extract() method. This happens // in the situation when "jaeger-debug-id" header is passed in the carrier to // the extract() method, but the request otherwise has no span context in it. // Previously this would've returned opentracing.ErrSpanContextNotFound from the // extract method, but now it returns a dummy context with only debugID filled in. // // See JaegerDebugHeader in constants.go // See TextMapPropagator#Extract func (c *SpanContext) isDebugIDContainerOnly() bool { return !c.traceID.IsValid() && c.debugID != "" } // ------- TraceID ------- func (t TraceID) String() string { if t.High == 0 { return fmt.Sprintf("%x", t.Low) } return fmt.Sprintf("%x%016x", t.High, t.Low) } // TraceIDFromString creates a TraceID from a hexadecimal string func TraceIDFromString(s string) (TraceID, error) { var hi, lo uint64 var err error if len(s) > 32 { return TraceID{}, fmt.Errorf("TraceID cannot be longer than 32 hex characters: %s", s) } else if len(s) > 16 { hiLen := len(s) - 16 if hi, err = strconv.ParseUint(s[0:hiLen], 16, 64); err != nil { return TraceID{}, err } if lo, err = strconv.ParseUint(s[hiLen:], 16, 64); err != nil { return TraceID{}, err } } else { if lo, err = strconv.ParseUint(s, 16, 64); err != nil { return TraceID{}, err } } return TraceID{High: hi, Low: lo}, nil } // IsValid checks if the trace ID is valid, i.e. not zero. func (t TraceID) IsValid() bool { return t.High != 0 || t.Low != 0 } // ------- SpanID ------- func (s SpanID) String() string { return fmt.Sprintf("%x", uint64(s)) } // SpanIDFromString creates a SpanID from a hexadecimal string func SpanIDFromString(s string) (SpanID, error) { if len(s) > 16 { return SpanID(0), fmt.Errorf("SpanID cannot be longer than 16 hex characters: %s", s) } id, err := strconv.ParseUint(s, 16, 64) if err != nil { return SpanID(0), err } return SpanID(id), nil }