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)