github.com/mailgun/holster/v4@v4.20.0/tracing/dummy_span_test.go (about) 1 package tracing_test 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/mailgun/holster/v4/tracing" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 "go.opentelemetry.io/otel/attribute" 12 sdktrace "go.opentelemetry.io/otel/sdk/trace" 13 "go.opentelemetry.io/otel/trace" 14 ) 15 16 func TestDummySpan(t *testing.T) { 17 ctx := context.Background() 18 withErrorAttr := trace.WithAttributes(attribute.Bool("error", true)) 19 20 t.Run("Single dropped span", func(t *testing.T) { 21 // Mock OTel exporter. 22 mockProcessor := new(MockSpanProcessor) 23 mockProcessor.On("Shutdown", mock.Anything).Once().Return(nil) 24 25 level := tracing.InfoLevel 26 setupMockTracerProvider(t, level, mockProcessor) 27 28 // Call code. 29 ctx1 := tracing.StartNamedScopeDebug(ctx, t.Name()) 30 tracing.EndScope(ctx1, nil) 31 32 err := tracing.CloseTracing(ctx) 33 require.NoError(t, err) 34 35 // Verify. 36 mockProcessor.AssertExpectations(t) 37 }) 38 39 t.Run("Nested scope with dropped leaf span", func(t *testing.T) { 40 // Mock OTel exporter. 41 mockProcessor := new(MockSpanProcessor) 42 matchFirstSpan := mock.MatchedBy(func(s sdktrace.ReadOnlySpan) bool { 43 return s.Name() == t.Name() 44 }) 45 mockProcessor.On("OnStart", mock.Anything, matchFirstSpan).Once() 46 mockProcessor.On("OnEnd", matchFirstSpan). 47 Once(). 48 Run(func(args mock.Arguments) { 49 s := args.Get(0).(sdktrace.ReadOnlySpan) 50 assertReadOnlySpanNoError(t, s) 51 assertHasLogLevel(t, tracing.InfoLevel, s) 52 }) 53 mockProcessor.On("Shutdown", mock.Anything).Once().Return(nil) 54 55 level := tracing.InfoLevel 56 setupMockTracerProvider(t, level, mockProcessor) 57 58 // Call code. 59 ctx1 := tracing.StartNamedScopeInfo(ctx, t.Name()) 60 ctx2 := tracing.StartNamedScopeDebug(ctx1, "Level 2 leaf dropped", withErrorAttr) 61 tracing.EndScope(ctx2, nil) 62 tracing.EndScope(ctx1, nil) 63 64 err := tracing.CloseTracing(ctx) 65 require.NoError(t, err) 66 67 // Verify. 68 mockProcessor.AssertExpectations(t) 69 }) 70 71 t.Run("Nested scopes with interleaved dropped span", func(t *testing.T) { 72 // Mock OTel exporter. 73 mockProcessor := new(MockSpanProcessor) 74 matchFirstSpan := mock.MatchedBy(func(s sdktrace.ReadOnlySpan) bool { 75 return s.Name() == "Level 1" 76 }) 77 matchLeafSpan := mock.MatchedBy(func(s sdktrace.ReadOnlySpan) bool { 78 return s.Name() == "Leaf" 79 }) 80 mockProcessor.On("OnStart", mock.Anything, matchFirstSpan).Once() 81 mockProcessor.On("OnStart", mock.Anything, matchLeafSpan).Once() 82 var firstSpan, leafSpan sdktrace.ReadOnlySpan 83 mockProcessor.On("OnEnd", matchFirstSpan). 84 Once(). 85 Run(func(args mock.Arguments) { 86 s := args.Get(0).(sdktrace.ReadOnlySpan) 87 assertReadOnlySpanNoError(t, s) 88 assertHasLogLevel(t, tracing.InfoLevel, s) 89 firstSpan = s 90 }) 91 mockProcessor.On("OnEnd", matchLeafSpan). 92 Once(). 93 Run(func(args mock.Arguments) { 94 s := args.Get(0).(sdktrace.ReadOnlySpan) 95 assertReadOnlySpanNoError(t, s) 96 assertHasLogLevel(t, tracing.InfoLevel, s) 97 leafSpan = s 98 }) 99 mockProcessor.On("Shutdown", mock.Anything).Once().Return(nil) 100 101 level := tracing.InfoLevel 102 setupMockTracerProvider(t, level, mockProcessor) 103 104 // Call code. 105 ctx1 := tracing.StartNamedScopeInfo(ctx, "Level 1") 106 ctx2 := tracing.StartNamedScopeDebug(ctx1, "Level 2 dropped", withErrorAttr) 107 ctx3 := tracing.StartNamedScopeInfo(ctx2, "Leaf") 108 tracing.EndScope(ctx3, nil) 109 tracing.EndScope(ctx2, nil) 110 tracing.EndScope(ctx1, nil) 111 112 err := tracing.CloseTracing(ctx) 113 require.NoError(t, err) 114 115 // Verify. 116 mockProcessor.AssertExpectations(t) 117 // Assert spans are linked: first -> leaf. 118 assert.Equal(t, firstSpan.SpanContext().SpanID(), leafSpan.Parent().SpanID()) 119 }) 120 121 t.Run("Nested scopes with multiple dropped leaf spans", func(t *testing.T) { 122 // Mock OTel exporter. 123 mockProcessor := new(MockSpanProcessor) 124 matchFirstSpan := mock.MatchedBy(func(s sdktrace.ReadOnlySpan) bool { 125 return s.Name() == "Level 1" 126 }) 127 mockProcessor.On("OnStart", mock.Anything, matchFirstSpan).Once() 128 mockProcessor.On("OnEnd", matchFirstSpan). 129 Once(). 130 Run(func(args mock.Arguments) { 131 s := args.Get(0).(sdktrace.ReadOnlySpan) 132 assertReadOnlySpanNoError(t, s) 133 assertHasLogLevel(t, tracing.InfoLevel, s) 134 }) 135 mockProcessor.On("Shutdown", mock.Anything).Once().Return(nil) 136 137 level := tracing.InfoLevel 138 setupMockTracerProvider(t, level, mockProcessor) 139 140 // Call code. 141 ctx1 := tracing.StartNamedScopeInfo(ctx, "Level 1") 142 ctx2 := tracing.StartNamedScopeDebug(ctx1, "Level 2 dropped", withErrorAttr) 143 ctx3 := tracing.StartNamedScopeDebug(ctx2, "Level 3 dropped", withErrorAttr) 144 ctx4 := tracing.StartNamedScopeDebug(ctx3, "leaf dropped", withErrorAttr) 145 tracing.EndScope(ctx4, nil) 146 tracing.EndScope(ctx3, nil) 147 tracing.EndScope(ctx2, nil) 148 tracing.EndScope(ctx1, nil) 149 150 err := tracing.CloseTracing(ctx) 151 require.NoError(t, err) 152 153 // Verify. 154 mockProcessor.AssertExpectations(t) 155 }) 156 157 t.Run("Nested scopes with multiple interleaved dropped spans", func(t *testing.T) { 158 // Mock OTel exporter. 159 mockProcessor := new(MockSpanProcessor) 160 matchFirstSpan := mock.MatchedBy(func(s sdktrace.ReadOnlySpan) bool { 161 return s.Name() == "Level 1" 162 }) 163 matchLeafSpan := mock.MatchedBy(func(s sdktrace.ReadOnlySpan) bool { 164 return s.Name() == "Leaf" 165 }) 166 mockProcessor.On("OnStart", mock.Anything, matchFirstSpan).Once() 167 mockProcessor.On("OnStart", mock.Anything, matchLeafSpan).Once() 168 var firstSpan, leafSpan sdktrace.ReadOnlySpan 169 mockProcessor.On("OnEnd", matchFirstSpan). 170 Once(). 171 Run(func(args mock.Arguments) { 172 s := args.Get(0).(sdktrace.ReadOnlySpan) 173 assertReadOnlySpanNoError(t, s) 174 assertHasLogLevel(t, tracing.InfoLevel, s) 175 firstSpan = s 176 }) 177 mockProcessor.On("OnEnd", matchLeafSpan). 178 Once(). 179 Run(func(args mock.Arguments) { 180 s := args.Get(0).(sdktrace.ReadOnlySpan) 181 assertReadOnlySpanNoError(t, s) 182 assertHasLogLevel(t, tracing.InfoLevel, s) 183 leafSpan = s 184 }) 185 mockProcessor.On("Shutdown", mock.Anything).Once().Return(nil) 186 187 level := tracing.InfoLevel 188 setupMockTracerProvider(t, level, mockProcessor) 189 190 // Call code. 191 ctx1 := tracing.StartNamedScopeInfo(ctx, "Level 1") 192 ctx2 := tracing.StartNamedScopeDebug(ctx1, "Level 2 dropped", withErrorAttr) 193 ctx3 := tracing.StartNamedScopeDebug(ctx2, "Level 3 dropped", withErrorAttr) 194 ctx4 := tracing.StartNamedScopeDebug(ctx3, "Level 4 dropped", withErrorAttr) 195 ctx5 := tracing.StartNamedScopeInfo(ctx4, "Leaf") 196 tracing.EndScope(ctx5, nil) 197 tracing.EndScope(ctx4, nil) 198 tracing.EndScope(ctx3, nil) 199 tracing.EndScope(ctx2, nil) 200 tracing.EndScope(ctx1, nil) 201 202 err := tracing.CloseTracing(ctx) 203 require.NoError(t, err) 204 205 // Verify. 206 mockProcessor.AssertExpectations(t) 207 // Assert spans are linked: first -> leaf. 208 assert.Equal(t, firstSpan.SpanContext().SpanID(), leafSpan.Parent().SpanID()) 209 }) 210 } 211 212 func assertHasLogLevel(t *testing.T, expectedLogLevel tracing.Level, s sdktrace.ReadOnlySpan) { 213 level, ok := levelFromReadOnlySpan(s) 214 if !ok { 215 t.Error("Error: Expected span log level to be defined") 216 return 217 } 218 219 assert.Equal(t, expectedLogLevel, level, "Span log level mismatch") 220 } 221 222 func assertReadOnlySpanNoError(t *testing.T, s sdktrace.ReadOnlySpan) { 223 for _, attr := range s.Attributes() { 224 if string(attr.Key) == "error" { 225 assert.True(t, attr.Value.AsBool()) 226 } 227 } 228 } 229 230 func levelFromReadOnlySpan(s sdktrace.ReadOnlySpan) (tracing.Level, bool) { 231 for _, attr := range s.Attributes() { 232 if string(attr.Key) == tracing.LogLevelKey { 233 level, err := tracing.ParseLogLevel(attr.Value.AsString()) 234 if err == nil { 235 return level, true 236 } 237 } 238 } 239 240 return tracing.Level(0), false 241 } 242 243 func setupMockTracerProvider(t *testing.T, level tracing.Level, mockProcessor *MockSpanProcessor) { 244 t.Setenv("OTEL_EXPORTERS", "none") 245 ctx := context.Background() 246 opts := []tracing.TracingOption{ 247 tracing.WithTracerProviderOption(sdktrace.WithSpanProcessor(mockProcessor)), 248 tracing.WithLevel(level), 249 } 250 err := tracing.InitTracing(ctx, "foobar", opts...) 251 require.NoError(t, err) 252 }