github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tracer/span_collector.go (about) 1 package tracer 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "strings" 9 10 "go.opentelemetry.io/otel/sdk/trace" 11 12 "github.com/tilt-dev/tilt/internal/tracer/exptel" 13 ) 14 15 // SpanCollector does 3 things: 16 // 1) Accepts spans from OpenTelemetry. 17 // 2) Stores spans (for now, in memory) 18 // 3) Allows consumers to read spans they might want to send elsewhere 19 // Numbers 2 and 3 access the same data, and so it's a concurrency issue. 20 type SpanCollector struct { 21 // members for communicating with the loop() goroutine 22 23 // for OpenTelemetry SpanCollector 24 spanDataCh chan trace.ReadOnlySpan 25 26 // for SpanSource 27 readReqCh chan chan []trace.ReadOnlySpan 28 requeueCh chan []trace.ReadOnlySpan 29 } 30 31 // SpanSource is the interface for consumers (generally telemetry.Controller) 32 type SpanSource interface { 33 // GetOutgoingSpans gives a consumer access to spans they should send 34 // If there are no outgoing spans, err will be io.EOF 35 // rejectFn allows client to reject spans, so they can be requeued 36 // rejectFn must be called, if at all, before the next call to GetOutgoingSpans 37 GetOutgoingSpans() (data io.Reader, rejectFn func(), err error) 38 39 // Close closes the SpanSource; the client may not interact with this SpanSource after calling Close 40 Close() error 41 } 42 43 func NewSpanCollector(ctx context.Context) *SpanCollector { 44 r := &SpanCollector{ 45 spanDataCh: make(chan trace.ReadOnlySpan), 46 readReqCh: make(chan chan []trace.ReadOnlySpan), 47 requeueCh: make(chan []trace.ReadOnlySpan), 48 } 49 go r.loop(ctx) 50 return r 51 } 52 53 func (c *SpanCollector) loop(ctx context.Context) { 54 // spans that have come in and are waiting to be read by a consumer 55 var queue []trace.ReadOnlySpan 56 57 for { 58 if c.spanDataCh == nil && c.readReqCh == nil { 59 return 60 } 61 select { 62 // New work coming in 63 case sd, ok := <-c.spanDataCh: 64 if !ok { 65 c.spanDataCh = nil 66 break 67 } 68 // add to the queue 69 queue = appendAndTrim(queue, sd) 70 case respCh, ok := <-c.readReqCh: 71 if !ok { 72 c.readReqCh = nil 73 break 74 } 75 // send the queue to the reader 76 respCh <- queue 77 queue = nil 78 // In-flight operations finishing 79 case sds := <-c.requeueCh: 80 queue = appendAndTrim(sds, queue...) 81 } 82 } 83 } 84 85 // OpenTelemetry exporter methods 86 87 func (c *SpanCollector) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error { 88 for _, s := range spans { 89 select { 90 case c.spanDataCh <- s: 91 case <-ctx.Done(): 92 return nil 93 } 94 } 95 return nil 96 } 97 98 func (c *SpanCollector) Shutdown(ctx context.Context) error { 99 close(c.spanDataCh) 100 return nil 101 } 102 103 // SpanSource 104 func (c *SpanCollector) GetOutgoingSpans() (io.Reader, func(), error) { 105 readCh := make(chan []trace.ReadOnlySpan) 106 c.readReqCh <- readCh 107 spans := <-readCh 108 109 if len(spans) == 0 { 110 return nil, nil, io.EOF 111 } 112 113 var b strings.Builder 114 w := json.NewEncoder(&b) 115 for i := range spans { 116 span := exptel.NewSpanFromOtel(spans[i], tracerName+"/") 117 if err := w.Encode(span); err != nil { 118 return nil, nil, fmt.Errorf("Error marshaling %v: %v", span, err) 119 } 120 } 121 122 rejectFn := func() { 123 c.requeueCh <- spans 124 } 125 126 return strings.NewReader(b.String()), rejectFn, nil 127 } 128 129 func (c *SpanCollector) Close() error { 130 close(c.readReqCh) 131 return nil 132 } 133 134 const maxQueueSize = 1024 // round number that can hold a fair bit of data 135 136 func appendAndTrim(lst1 []trace.ReadOnlySpan, lst2 ...trace.ReadOnlySpan) []trace.ReadOnlySpan { 137 r := append(lst1, lst2...) 138 if len(r) <= maxQueueSize { 139 return r 140 } 141 elemsToRemove := len(r) - maxQueueSize 142 return r[elemsToRemove:] 143 } 144 145 var _ trace.SpanExporter = (*SpanCollector)(nil) 146 var _ SpanSource = (*SpanCollector)(nil)