pkg/sampling/oteltracestate.go (139 lines of code) (raw):

// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 package sampling // import "github.com/open-telemetry/opentelemetry-collector-contrib/pkg/sampling" import ( "errors" "io" "regexp" "strconv" ) // OpenTelemetryTraceState represents the `ot` section of the W3C tracestate // which is specified generically in https://opentelemetry.io/docs/specs/otel/trace/tracestate-handling/. // // OpenTelemetry defines two specific values that convey sampling // probability, known as T-Value (with "th", for threshold), R-Value // (with key "rv", for random value), and extra values. type OpenTelemetryTraceState struct { commonTraceState // sampling r and t-values rnd Randomness // r value parsed, as unsigned rvalue string // 14 ASCII hex digits threshold Threshold // t value parsed, as a threshold tvalue string // 1-14 ASCII hex digits } const ( // rValueFieldName is the OTel tracestate field for R-value rValueFieldName = "rv" // tValueFieldName is the OTel tracestate field for T-value tValueFieldName = "th" // hardMaxOTelLength is the maximum encoded size of an OTel // tracestate value. hardMaxOTelLength = 256 // chr = ucalpha / lcalpha / DIGIT / "." / "_" / "-" // ucalpha = %x41-5A ; A-Z // lcalpha = %x61-7A ; a-z // key = lcalpha *(lcalpha / DIGIT ) // value = *(chr) // list-member = key ":" value // list = list-member *( ";" list-member ) otelKeyRegexp = lcAlphaRegexp + lcAlphanumRegexp + `*` otelValueRegexp = `[a-zA-Z0-9._\-]*` otelMemberRegexp = `(?:` + otelKeyRegexp + `:` + otelValueRegexp + `)` otelSemicolonMemberRegexp = `(?:` + `;` + otelMemberRegexp + `)` otelTracestateRegexp = `^` + otelMemberRegexp + otelSemicolonMemberRegexp + `*$` ) var ( otelTracestateRe = regexp.MustCompile(otelTracestateRegexp) otelSyntax = keyValueScanner{ maxItems: -1, trim: false, separator: ';', equality: ':', } // ErrInconsistentSampling is returned when a sampler update // is illogical, indicating that the tracestate was not // modified. Preferably, Samplers will avoid seeing this // error by using a ThresholdGreater() test, which allows them // to report a more clear error to the user. For example, if // data arrives sampled at 1/100 and an equalizing sampler is // configured for 1/2 sampling, the Sampler may detect the // illogical condition itself using ThresholdGreater and skip // the call to UpdateTValueWithSampling, which will have no // effect and return this error. How a sampler decides to // handle this condition is up to the sampler: for example the // equalizing sampler can decide to pass through a span // indicating 1/100 sampling or it can reject the span. ErrInconsistentSampling = errors.New("cannot raise existing sampling probability") ) // NewOpenTelemetryTraceState returns a parsed representation of the // OpenTelemetry tracestate section. Errors indicate an invalid // tracestate was received. func NewOpenTelemetryTraceState(input string) (OpenTelemetryTraceState, error) { otts := OpenTelemetryTraceState{} if len(input) > hardMaxOTelLength { return otts, ErrTraceStateSize } if !otelTracestateRe.MatchString(input) { return otts, strconv.ErrSyntax } err := otelSyntax.scanKeyValues(input, func(key, value string) error { var err error switch key { case rValueFieldName: if otts.rnd, err = RValueToRandomness(value); err == nil { otts.rvalue = value } else { // RValueRandomness() will return false, the error // accumulates and is returned below. otts.rvalue = "" otts.rnd = Randomness{} } case tValueFieldName: if otts.threshold, err = TValueToThreshold(value); err == nil { otts.tvalue = value } else { // TValueThreshold() will return false, the error // accumulates and is returned below. otts.tvalue = "" otts.threshold = AlwaysSampleThreshold } default: otts.kvs = append(otts.kvs, KV{ Key: key, Value: value, }) } return err }) return otts, err } // RValue returns the R-value (key: "rv") as a string or empty if // there is no R-value set. func (otts *OpenTelemetryTraceState) RValue() string { return otts.rvalue } // RValueRandomness returns the randomness object corresponding with // RValue() and a boolean indicating whether the R-value is set. func (otts *OpenTelemetryTraceState) RValueRandomness() (Randomness, bool) { return otts.rnd, len(otts.rvalue) != 0 } // TValue returns the T-value (key: "th") as a string or empty if // there is no T-value set. func (otts *OpenTelemetryTraceState) TValue() string { return otts.tvalue } // TValueThreshold returns the threshold object corresponding with // TValue() and a boolean (equal to len(TValue()) != 0 indicating // whether the T-value is valid. func (otts *OpenTelemetryTraceState) TValueThreshold() (Threshold, bool) { return otts.threshold, len(otts.tvalue) != 0 } // UpdateTValueWithSampling modifies the TValue of this object, which // changes its adjusted count. It is not logical to modify a sampling // probability in the direction of larger probability. This prevents // accidental loss of adjusted count. // // If the change of TValue leads to inconsistency, an error is returned. func (otts *OpenTelemetryTraceState) UpdateTValueWithSampling(sampledThreshold Threshold) error { // Note: there was once a code path here that optimized for // cases where a static threshold is used, in which case the // call to TValue() causes an unnecessary allocation per data // item (w/ a constant result). We have eliminated that // parameter, due to the significant potential for mis-use. // Therefore, this method always recomputes TValue() of the // sampledThreshold (on success). A future method such as // UpdateTValueWithSamplingFixedTValue() could extend this // API to address this allocation, although it is probably // not significant. if len(otts.TValue()) != 0 && ThresholdGreater(otts.threshold, sampledThreshold) { return ErrInconsistentSampling } // Note NeverSampleThreshold is the (exclusive) upper boundary // of valid thresholds, so the test above permits never- // sampled updates, in which case the TValue() here is empty. otts.threshold = sampledThreshold otts.tvalue = sampledThreshold.TValue() return nil } // AdjustedCount returns the adjusted count for this item. If the // TValue string is empty, this returns 0, otherwise returns // Threshold.AdjustedCount(). func (otts *OpenTelemetryTraceState) AdjustedCount() float64 { if len(otts.tvalue) == 0 { // Note: this case covers the zero state, where // len(tvalue) == 0 and threshold == AlwaysSampleThreshold. // We return 0 to indicate that no information is available. return 0 } return otts.threshold.AdjustedCount() } // ClearTValue is used to unset TValue, for use in cases where it is // inconsistent on arrival. func (otts *OpenTelemetryTraceState) ClearTValue() { otts.tvalue = "" otts.threshold = Threshold{} } // SetRValue establishes explicit randomness for this TraceState. func (otts *OpenTelemetryTraceState) SetRValue(randomness Randomness) { otts.rnd = randomness otts.rvalue = randomness.RValue() } // ClearRValue unsets explicit randomness. func (otts *OpenTelemetryTraceState) ClearRValue() { otts.rvalue = "" otts.rnd = Randomness{} } // HasAnyValue returns true if there are any fields in this // tracestate, including any extra values. func (otts *OpenTelemetryTraceState) HasAnyValue() bool { return len(otts.RValue()) != 0 || len(otts.TValue()) != 0 || len(otts.ExtraValues()) != 0 } // Serialize encodes this TraceState object. func (otts *OpenTelemetryTraceState) Serialize(w io.StringWriter) error { ser := serializer{writer: w} cnt := 0 sep := func() { if cnt != 0 { ser.write(";") } cnt++ } if len(otts.RValue()) != 0 { sep() ser.write(rValueFieldName) ser.write(":") ser.write(otts.RValue()) } if len(otts.TValue()) != 0 { sep() ser.write(tValueFieldName) ser.write(":") ser.write(otts.TValue()) } for _, kv := range otts.ExtraValues() { sep() ser.write(kv.Key) ser.write(":") ser.write(kv.Value) } return ser.err }