go.opentelemetry.io/contrib/instrumentation/github.com/Shopify/sarama/otelsarama@v0.43.0/producer.go (about)

     1  // Copyright The OpenTelemetry Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package otelsarama // import "go.opentelemetry.io/contrib/instrumentation/github.com/Shopify/sarama/otelsarama"
    16  
    17  import (
    18  	"context"
    19  	"encoding/binary"
    20  	"fmt"
    21  	"strconv"
    22  	"sync"
    23  
    24  	"github.com/Shopify/sarama"
    25  
    26  	"go.opentelemetry.io/otel/codes"
    27  
    28  	"go.opentelemetry.io/otel/attribute"
    29  	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
    30  	"go.opentelemetry.io/otel/trace"
    31  )
    32  
    33  type syncProducer struct {
    34  	sarama.SyncProducer
    35  	cfg          config
    36  	saramaConfig *sarama.Config
    37  }
    38  
    39  // SendMessage calls sarama.SyncProducer.SendMessage and traces the request.
    40  func (p *syncProducer) SendMessage(msg *sarama.ProducerMessage) (partition int32, offset int64, err error) {
    41  	span := startProducerSpan(p.cfg, p.saramaConfig.Version, msg)
    42  	partition, offset, err = p.SyncProducer.SendMessage(msg)
    43  	finishProducerSpan(span, partition, offset, err)
    44  	return partition, offset, err
    45  }
    46  
    47  // SendMessages calls sarama.SyncProducer.SendMessages and traces the requests.
    48  func (p *syncProducer) SendMessages(msgs []*sarama.ProducerMessage) error {
    49  	// Although there's only one call made to the SyncProducer, the messages are
    50  	// treated individually, so we create a span for each one
    51  	spans := make([]trace.Span, len(msgs))
    52  	for i, msg := range msgs {
    53  		spans[i] = startProducerSpan(p.cfg, p.saramaConfig.Version, msg)
    54  	}
    55  	err := p.SyncProducer.SendMessages(msgs)
    56  	for i, span := range spans {
    57  		finishProducerSpan(span, msgs[i].Partition, msgs[i].Offset, err)
    58  	}
    59  	return err
    60  }
    61  
    62  // WrapSyncProducer wraps a sarama.SyncProducer so that all produced messages
    63  // are traced.
    64  func WrapSyncProducer(saramaConfig *sarama.Config, producer sarama.SyncProducer, opts ...Option) sarama.SyncProducer {
    65  	cfg := newConfig(opts...)
    66  	if saramaConfig == nil {
    67  		saramaConfig = sarama.NewConfig()
    68  	}
    69  
    70  	return &syncProducer{
    71  		SyncProducer: producer,
    72  		cfg:          cfg,
    73  		saramaConfig: saramaConfig,
    74  	}
    75  }
    76  
    77  type asyncProducer struct {
    78  	sarama.AsyncProducer
    79  	input         chan *sarama.ProducerMessage
    80  	successes     chan *sarama.ProducerMessage
    81  	errors        chan *sarama.ProducerError
    82  	closeErr      chan error
    83  	closeSig      chan struct{}
    84  	closeAsyncSig chan struct{}
    85  }
    86  
    87  // Input returns the input channel.
    88  func (p *asyncProducer) Input() chan<- *sarama.ProducerMessage {
    89  	return p.input
    90  }
    91  
    92  // Successes returns the successes channel.
    93  func (p *asyncProducer) Successes() <-chan *sarama.ProducerMessage {
    94  	return p.successes
    95  }
    96  
    97  // Errors returns the errors channel.
    98  func (p *asyncProducer) Errors() <-chan *sarama.ProducerError {
    99  	return p.errors
   100  }
   101  
   102  // AsyncClose async close producer.
   103  func (p *asyncProducer) AsyncClose() {
   104  	close(p.input)
   105  	close(p.closeAsyncSig)
   106  }
   107  
   108  // Close shuts down the producer and waits for any buffered messages to be
   109  // flushed.
   110  //
   111  // Due to the implement of sarama, some messages may lose successes or errors status
   112  // while closing.
   113  func (p *asyncProducer) Close() error {
   114  	close(p.input)
   115  	close(p.closeSig)
   116  	return <-p.closeErr
   117  }
   118  
   119  type producerMessageContext struct {
   120  	span           trace.Span
   121  	metadataBackup interface{}
   122  }
   123  
   124  // WrapAsyncProducer wraps a sarama.AsyncProducer so that all produced messages
   125  // are traced. It requires the underlying sarama Config, so we can know whether
   126  // or not successes will be returned.
   127  //
   128  // If `Return.Successes` is false, there is no way to know partition and offset of
   129  // the message.
   130  func WrapAsyncProducer(saramaConfig *sarama.Config, p sarama.AsyncProducer, opts ...Option) sarama.AsyncProducer {
   131  	cfg := newConfig(opts...)
   132  	if saramaConfig == nil {
   133  		saramaConfig = sarama.NewConfig()
   134  	}
   135  
   136  	wrapped := &asyncProducer{
   137  		AsyncProducer: p,
   138  		input:         make(chan *sarama.ProducerMessage),
   139  		successes:     make(chan *sarama.ProducerMessage),
   140  		errors:        make(chan *sarama.ProducerError),
   141  		closeErr:      make(chan error),
   142  		closeSig:      make(chan struct{}),
   143  		closeAsyncSig: make(chan struct{}),
   144  	}
   145  
   146  	var (
   147  		mtx                     sync.Mutex
   148  		producerMessageContexts = make(map[interface{}]producerMessageContext)
   149  	)
   150  
   151  	// Spawn Input producer goroutine.
   152  	go func() {
   153  		for {
   154  			select {
   155  			case <-wrapped.closeSig:
   156  				wrapped.closeErr <- p.Close()
   157  				return
   158  			case <-wrapped.closeAsyncSig:
   159  				p.AsyncClose()
   160  				return
   161  			case msg, ok := <-wrapped.input:
   162  				if !ok {
   163  					continue // wait for closeAsyncSig
   164  				}
   165  				span := startProducerSpan(cfg, saramaConfig.Version, msg)
   166  
   167  				// Create message context, backend message metadata
   168  				mc := producerMessageContext{
   169  					metadataBackup: msg.Metadata,
   170  					span:           span,
   171  				}
   172  
   173  				// Remember metadata using span ID as a cache key
   174  				msg.Metadata = span.SpanContext().SpanID()
   175  				if saramaConfig.Producer.Return.Successes {
   176  					mtx.Lock()
   177  					producerMessageContexts[msg.Metadata] = mc
   178  					mtx.Unlock()
   179  				} else {
   180  					// If returning successes isn't enabled, we just finish the
   181  					// span right away because there's no way to know when it will
   182  					// be done.
   183  					mc.span.End()
   184  				}
   185  
   186  				p.Input() <- msg
   187  			}
   188  		}
   189  	}()
   190  
   191  	// Sarama will consume all the successes and errors by itself while closing,
   192  	// so we need to end these spans ourselves.
   193  	var cleanupWg sync.WaitGroup
   194  
   195  	// Spawn Successes consumer goroutine.
   196  	cleanupWg.Add(1)
   197  	go func() {
   198  		defer func() {
   199  			close(wrapped.successes)
   200  			cleanupWg.Done()
   201  		}()
   202  		for msg := range p.Successes() {
   203  			key := msg.Metadata
   204  			mtx.Lock()
   205  			if mc, ok := producerMessageContexts[key]; ok {
   206  				delete(producerMessageContexts, key)
   207  				finishProducerSpan(mc.span, msg.Partition, msg.Offset, nil)
   208  				msg.Metadata = mc.metadataBackup // Restore message metadata
   209  			}
   210  			mtx.Unlock()
   211  			wrapped.successes <- msg
   212  		}
   213  	}()
   214  
   215  	// Spawn Errors consumer goroutine.
   216  	cleanupWg.Add(1)
   217  	go func() {
   218  		defer func() {
   219  			close(wrapped.errors)
   220  			cleanupWg.Done()
   221  		}()
   222  		for errMsg := range p.Errors() {
   223  			key := errMsg.Msg.Metadata
   224  			mtx.Lock()
   225  			if mc, ok := producerMessageContexts[key]; ok {
   226  				delete(producerMessageContexts, key)
   227  				finishProducerSpan(mc.span, errMsg.Msg.Partition, errMsg.Msg.Offset, errMsg.Err)
   228  				errMsg.Msg.Metadata = mc.metadataBackup // Restore message metadata
   229  			}
   230  			mtx.Unlock()
   231  			wrapped.errors <- errMsg
   232  		}
   233  	}()
   234  
   235  	// Spawn spans cleanup goroutine.
   236  	go func() {
   237  		// wait until both consumer goroutines are closed
   238  		cleanupWg.Wait()
   239  		// end all remaining spans
   240  		mtx.Lock()
   241  		for _, mc := range producerMessageContexts {
   242  			mc.span.End()
   243  		}
   244  		mtx.Unlock()
   245  	}()
   246  
   247  	return wrapped
   248  }
   249  
   250  // msgPayloadSize returns the approximate estimate of message size in bytes.
   251  //
   252  // For kafka version <= 0.10, the message size is
   253  // ~ 4(crc32) + 8(timestamp) + 4(key len) + 4(value len) + 4(message len) + 1(attrs) + 1(magic).
   254  //
   255  // For kafka version >= 0.11, the message size with varint encoding is
   256  // ~ 5 * (crc32, key len, value len, message len, attrs) + timestamp + 1 byte (magic).
   257  // + header key + header value + header key len + header value len.
   258  func msgPayloadSize(msg *sarama.ProducerMessage, kafkaVersion sarama.KafkaVersion) int {
   259  	maximumRecordOverhead := 5*binary.MaxVarintLen32 + binary.MaxVarintLen64 + 1
   260  	producerMessageOverhead := 26
   261  	version := 1
   262  	if kafkaVersion.IsAtLeast(sarama.V0_11_0_0) {
   263  		version = 2
   264  	}
   265  	size := producerMessageOverhead
   266  	if version >= 2 {
   267  		size = maximumRecordOverhead
   268  		for _, h := range msg.Headers {
   269  			size += len(h.Key) + len(h.Value) + 2*binary.MaxVarintLen32
   270  		}
   271  	}
   272  	if msg.Key != nil {
   273  		size += msg.Key.Length()
   274  	}
   275  	if msg.Value != nil {
   276  		size += msg.Value.Length()
   277  	}
   278  	return size
   279  }
   280  
   281  func startProducerSpan(cfg config, version sarama.KafkaVersion, msg *sarama.ProducerMessage) trace.Span {
   282  	// If there's a span context in the message, use that as the parent context.
   283  	carrier := NewProducerMessageCarrier(msg)
   284  	ctx := cfg.Propagators.Extract(context.Background(), carrier)
   285  
   286  	// Create a span.
   287  	attrs := []attribute.KeyValue{
   288  		semconv.MessagingSystem("kafka"),
   289  		semconv.MessagingDestinationKindTopic,
   290  		semconv.MessagingDestinationName(msg.Topic),
   291  		semconv.MessagingMessagePayloadSizeBytes(msgPayloadSize(msg, version)),
   292  		semconv.MessagingOperationPublish,
   293  	}
   294  	opts := []trace.SpanStartOption{
   295  		trace.WithAttributes(attrs...),
   296  		trace.WithSpanKind(trace.SpanKindProducer),
   297  	}
   298  	ctx, span := cfg.Tracer.Start(ctx, fmt.Sprintf("%s publish", msg.Topic), opts...)
   299  
   300  	if version.IsAtLeast(sarama.V0_11_0_0) {
   301  		// Inject current span context, so consumers can use it to propagate span.
   302  		cfg.Propagators.Inject(ctx, carrier)
   303  	}
   304  
   305  	return span
   306  }
   307  
   308  func finishProducerSpan(span trace.Span, partition int32, offset int64, err error) {
   309  	span.SetAttributes(
   310  		semconv.MessagingMessageID(strconv.FormatInt(offset, 10)),
   311  		semconv.MessagingKafkaDestinationPartition(int(partition)),
   312  	)
   313  	if err != nil {
   314  		span.SetStatus(codes.Error, err.Error())
   315  	}
   316  	span.End()
   317  }