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 }