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  }