github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tracer/span_collector_test.go (about)

     1  package tracer
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"io"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	sdktrace "go.opentelemetry.io/otel/sdk/trace"
    13  	"go.opentelemetry.io/otel/sdk/trace/tracetest"
    14  	"go.opentelemetry.io/otel/trace"
    15  )
    16  
    17  func TestExporterSimple(t *testing.T) {
    18  	f := newFixture(t)
    19  
    20  	sd1 := sd(1)
    21  	f.export(sd1)
    22  
    23  	f.assertConsumeSpans(sd1)
    24  
    25  	sd2 := sd(2)
    26  	f.export(sd2)
    27  	f.assertConsumeSpans(sd2)
    28  }
    29  
    30  func TestExporterReject(t *testing.T) {
    31  	f := newFixture(t)
    32  
    33  	sd1 := sd(1)
    34  	f.export(sd1)
    35  
    36  	f.assertRejectSpans(sd1)
    37  	f.assertRejectSpans(sd1)
    38  	f.assertRejectSpans(sd1)
    39  	sd2 := sd(2)
    40  	f.export(sd2)
    41  	f.assertRejectSpans(sd1, sd2)
    42  	f.assertConsumeSpans(sd1, sd2)
    43  }
    44  
    45  // one test that makes sure the final string we're seeing is reasonable
    46  func TestExporterString(t *testing.T) {
    47  	f := newFixture(t)
    48  
    49  	spanID, _ := trace.SpanIDFromHex("00f067aa0ba902b7")
    50  	sd := tracetest.SpanStub{
    51  		SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
    52  			SpanID: spanID,
    53  		}),
    54  		Name: "foo",
    55  	}.Snapshot()
    56  
    57  	f.export(sd)
    58  	s, _ := f.getSpanText()
    59  	// N.B. we add a `tilt.usage/` prefix to the span name during export
    60  	expected := `{"SpanContext":{"TraceID":"00000000000000000000000000000000","SpanID":"00f067aa0ba902b7","TraceFlags":0},"ParentSpanID":"0000000000000000","SpanKind":0,"Name":"tilt.dev/usage/foo","StartTime":"0001-01-01T00:00:00Z","EndTime":"0001-01-01T00:00:00Z","Attributes":null,"MessageEvents":null,"Links":null,"Status":0,"HasRemoteParent":false,"DroppedAttributeCount":0,"DroppedMessageEventCount":0,"DroppedLinkCount":0,"ChildSpanCount":0}
    61  `
    62  
    63  	require.JSONEq(t, expected, s, "spans did not match")
    64  }
    65  
    66  func TestExporterTrims(t *testing.T) {
    67  	f := newFixture(t)
    68  
    69  	var sds []sdktrace.ReadOnlySpan
    70  	for i := 0; i < 2048; i++ {
    71  		sdi := sd(i)
    72  		sds = append(sds, sdi)
    73  		f.export(sdi)
    74  	}
    75  
    76  	f.assertConsumeSpans(sds[1024:]...)
    77  }
    78  
    79  func TestExporterStartsEmpty(t *testing.T) {
    80  	f := newFixture(t)
    81  
    82  	f.assertEmpty()
    83  	f.assertEmpty()
    84  	sd1 := sd(1)
    85  	f.export(sd1)
    86  	sd2 := sd(2)
    87  	f.export(sd2)
    88  
    89  	f.assertConsumeSpans(sd1, sd2)
    90  	f.assertEmpty()
    91  	f.assertEmpty()
    92  }
    93  
    94  type fixture struct {
    95  	t   *testing.T
    96  	ctx context.Context
    97  	sc  *SpanCollector
    98  }
    99  
   100  func newFixture(t *testing.T) *fixture {
   101  	ctx := context.Background()
   102  	sc := NewSpanCollector(ctx)
   103  	ret := &fixture{
   104  		t:   t,
   105  		ctx: ctx,
   106  		sc:  sc,
   107  	}
   108  
   109  	t.Cleanup(ret.tearDown)
   110  	return ret
   111  }
   112  
   113  func (f *fixture) tearDown() {
   114  	f.t.Helper()
   115  	f.assertEmpty()
   116  	require.NoError(f.t, f.sc.Shutdown(f.ctx))
   117  	require.NoError(f.t, f.sc.Close())
   118  }
   119  
   120  func (f *fixture) export(sd sdktrace.ReadOnlySpan) {
   121  	f.t.Helper()
   122  	require.NoError(f.t, f.sc.ExportSpans(f.ctx, []sdktrace.ReadOnlySpan{sd}))
   123  }
   124  
   125  func (f *fixture) assertConsumeSpans(expected ...sdktrace.ReadOnlySpan) {
   126  	f.t.Helper()
   127  	actual, _ := f.getSpans()
   128  
   129  	f.assertSpansEqual(expected, actual)
   130  }
   131  
   132  func (f *fixture) assertRejectSpans(expected ...sdktrace.ReadOnlySpan) {
   133  	f.t.Helper()
   134  	actual, rejectFn := f.getSpans()
   135  	rejectFn()
   136  
   137  	f.assertSpansEqual(expected, actual)
   138  }
   139  
   140  func (f *fixture) assertEmpty() {
   141  	f.t.Helper()
   142  	r, _, err := f.sc.GetOutgoingSpans()
   143  	if err != io.EOF {
   144  		f.t.Fatalf("spans not empty: %v %v", r, err)
   145  	}
   146  }
   147  
   148  func (f *fixture) assertSpansEqual(expected []sdktrace.ReadOnlySpan, actual []sdktrace.ReadOnlySpan) {
   149  	f.t.Helper()
   150  	if len(expected) != len(actual) {
   151  		f.t.Fatalf("got %v (len %v); expected %v (len %v)", actual, len(actual), expected, len(expected))
   152  	}
   153  
   154  	for i, ex := range expected {
   155  		act := actual[i]
   156  
   157  		exJSON, exErr := json.MarshalIndent(ex, "", "  ")
   158  		actJSON, actErr := json.MarshalIndent(act, "", "  ")
   159  		if exErr != nil || actErr != nil {
   160  			f.t.Fatalf("unexpected error %v %v", exErr, actErr)
   161  		}
   162  		assert.JSONEq(f.t, string(exJSON), string(actJSON), "unequal spans")
   163  	}
   164  }
   165  
   166  func (f *fixture) getSpanText() (string, func()) {
   167  	f.t.Helper()
   168  	r, rejectFn, err := f.sc.GetOutgoingSpans()
   169  	if err != nil {
   170  		f.t.Fatalf("unexpected error %v", err)
   171  	}
   172  
   173  	bs, err := io.ReadAll(r)
   174  	if err != nil {
   175  		f.t.Fatalf("unexpected error %v", err)
   176  	}
   177  
   178  	return string(bs), rejectFn
   179  }
   180  
   181  func (f *fixture) getSpans() ([]sdktrace.ReadOnlySpan, func()) {
   182  	f.t.Helper()
   183  	s, rejectFn := f.getSpanText()
   184  	r := strings.NewReader(s)
   185  	dec := json.NewDecoder(r)
   186  
   187  	var result []sdktrace.ReadOnlySpan
   188  
   189  	for dec.More() {
   190  		var data SpanDataFromJSON
   191  		if err := dec.Decode(&data); err != nil {
   192  			f.t.Fatalf("unexpected error %v %q", err, s)
   193  		}
   194  		result = append(result, sdFromData(data))
   195  	}
   196  
   197  	if len(result) == 0 {
   198  		f.t.Fatalf("Got an empty string from a non-nil Reader")
   199  	}
   200  
   201  	return result, rejectFn
   202  }
   203  
   204  type SpanDataFromJSON struct {
   205  	SpanContext SpanContextFromJSON
   206  }
   207  
   208  type SpanContextFromJSON struct {
   209  	SpanID string
   210  }
   211  
   212  func sdFromData(data SpanDataFromJSON) sdktrace.ReadOnlySpan {
   213  	spanID, _ := trace.SpanIDFromHex(data.SpanContext.SpanID)
   214  
   215  	return tracetest.SpanStub{
   216  		SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
   217  			SpanID: spanID,
   218  		}),
   219  	}.Snapshot()
   220  }
   221  
   222  func sd(id int) sdktrace.ReadOnlySpan {
   223  	return tracetest.SpanStub{
   224  		SpanContext: trace.NewSpanContext(trace.SpanContextConfig{
   225  			SpanID: idFromInt(id),
   226  		}),
   227  	}.Snapshot()
   228  }
   229  
   230  func idFromInt(id int) (r trace.SpanID) {
   231  	r[7] = uint8(id % 256)
   232  	r[6] = uint8(id / 256)
   233  	return r
   234  }