// Copyright The OpenTelemetry Authors // // Licensed 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 trace import ( "context" "fmt" "math/rand" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" ) func TestParentBasedDefaultLocalParentSampled(t *testing.T) { sampler := ParentBased(AlwaysSample()) traceID, _ := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736") spanID, _ := trace.SpanIDFromHex("00f067aa0ba902b7") parentCtx := trace.ContextWithSpanContext( context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled, }), ) if sampler.ShouldSample(SamplingParameters{ParentContext: parentCtx}).Decision != RecordAndSample { t.Error("Sampling decision should be RecordAndSample") } } func TestParentBasedDefaultLocalParentNotSampled(t *testing.T) { sampler := ParentBased(AlwaysSample()) traceID, _ := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736") spanID, _ := trace.SpanIDFromHex("00f067aa0ba902b7") parentCtx := trace.ContextWithSpanContext( context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ TraceID: traceID, SpanID: spanID, }), ) if sampler.ShouldSample(SamplingParameters{ParentContext: parentCtx}).Decision != Drop { t.Error("Sampling decision should be Drop") } } func TestParentBasedWithNoParent(t *testing.T) { params := SamplingParameters{} sampler := ParentBased(AlwaysSample()) if sampler.ShouldSample(params).Decision != RecordAndSample { t.Error("Sampling decision should be RecordAndSample") } sampler = ParentBased(NeverSample()) if sampler.ShouldSample(params).Decision != Drop { t.Error("Sampling decision should be Drop") } } func TestParentBasedWithSamplerOptions(t *testing.T) { testCases := []struct { name string samplerOption ParentBasedSamplerOption isParentRemote, isParentSampled bool expectedDecision SamplingDecision }{ { "localParentSampled", WithLocalParentSampled(NeverSample()), false, true, Drop, }, { "localParentNotSampled", WithLocalParentNotSampled(AlwaysSample()), false, false, RecordAndSample, }, { "remoteParentSampled", WithRemoteParentSampled(NeverSample()), true, true, Drop, }, { "remoteParentNotSampled", WithRemoteParentNotSampled(AlwaysSample()), true, false, RecordAndSample, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { traceID, _ := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736") spanID, _ := trace.SpanIDFromHex("00f067aa0ba902b7") pscc := trace.SpanContextConfig{ TraceID: traceID, SpanID: spanID, Remote: tc.isParentRemote, } if tc.isParentSampled { pscc.TraceFlags = trace.FlagsSampled } params := SamplingParameters{ ParentContext: trace.ContextWithSpanContext( context.Background(), trace.NewSpanContext(pscc), ), } sampler := ParentBased( nil, tc.samplerOption, ) var wantStr, gotStr string switch tc.expectedDecision { case RecordAndSample: wantStr = "RecordAndSample" case Drop: wantStr = "Drop" default: wantStr = "unknown" } actualDecision := sampler.ShouldSample(params).Decision switch actualDecision { case RecordAndSample: gotStr = "RecordAndSample" case Drop: gotStr = "Drop" default: gotStr = "unknown" } assert.Equalf(t, tc.expectedDecision, actualDecision, "want %s, got %s", wantStr, gotStr) }) } } func TestParentBasedDefaultDescription(t *testing.T) { sampler := ParentBased(AlwaysSample()) expectedDescription := fmt.Sprintf("ParentBased{root:%s,remoteParentSampled:%s,"+ "remoteParentNotSampled:%s,localParentSampled:%s,localParentNotSampled:%s}", AlwaysSample().Description(), AlwaysSample().Description(), NeverSample().Description(), AlwaysSample().Description(), NeverSample().Description()) if sampler.Description() != expectedDescription { t.Errorf("Sampler description should be %s, got '%s' instead", expectedDescription, sampler.Description(), ) } } // TraceIDRatioBased sampler requirements state // // "A TraceIDRatioBased sampler with a given sampling rate MUST also sample // all traces that any TraceIDRatioBased sampler with a lower sampling rate // would sample." func TestTraceIdRatioSamplesInclusively(t *testing.T) { const ( numSamplers = 1000 numTraces = 100 ) idg := defaultIDGenerator() for i := 0; i < numSamplers; i++ { ratioLo, ratioHi := rand.Float64(), rand.Float64() if ratioHi < ratioLo { ratioLo, ratioHi = ratioHi, ratioLo } samplerHi := TraceIDRatioBased(ratioHi) samplerLo := TraceIDRatioBased(ratioLo) for j := 0; j < numTraces; j++ { traceID, _ := idg.NewIDs(context.Background()) params := SamplingParameters{TraceID: traceID} if samplerLo.ShouldSample(params).Decision == RecordAndSample { require.Equal(t, RecordAndSample, samplerHi.ShouldSample(params).Decision, "%s sampled but %s did not", samplerLo.Description(), samplerHi.Description()) } } } } func TestTracestateIsPassed(t *testing.T) { testCases := []struct { name string sampler Sampler }{ { "notSampled", NeverSample(), }, { "sampled", AlwaysSample(), }, { "parentSampled", ParentBased(AlwaysSample()), }, { "parentNotSampled", ParentBased(NeverSample()), }, { "traceIDRatioSampler", TraceIDRatioBased(.5), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { traceState, err := trace.ParseTraceState("k=v") if err != nil { t.Error(err) } params := SamplingParameters{ ParentContext: trace.ContextWithSpanContext( context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ TraceState: traceState, }), ), } require.Equal(t, traceState, tc.sampler.ShouldSample(params).Tracestate, "TraceState is not equal") }) } }