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 }