github.com/waldiirawan/apm-agent-go/v2@v2.2.2/span_compressed.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package apm // import "github.com/waldiirawan/apm-agent-go/v2" 19 20 import ( 21 "sync/atomic" 22 "time" 23 24 "github.com/waldiirawan/apm-agent-go/v2/model" 25 ) 26 27 const ( 28 _ int = iota 29 compressedStrategyExactMatch 30 compressedStrategySameKind 31 ) 32 33 const ( 34 compressedSpanSameKindName = "Calls to " 35 ) 36 37 type compositeSpan struct { 38 lastSiblingEndTime time.Time 39 // this internal representation should be set in Nanoseconds, although 40 // the model unit is set in Milliseconds. 41 sum time.Duration 42 count int 43 compressionStrategy int 44 } 45 46 func (cs compositeSpan) build() *model.CompositeSpan { 47 var out model.CompositeSpan 48 switch cs.compressionStrategy { 49 case compressedStrategyExactMatch: 50 out.CompressionStrategy = "exact_match" 51 case compressedStrategySameKind: 52 out.CompressionStrategy = "same_kind" 53 } 54 out.Count = cs.count 55 out.Sum = float64(cs.sum) / float64(time.Millisecond) 56 return &out 57 } 58 59 func (cs compositeSpan) empty() bool { 60 return cs.count < 1 61 } 62 63 // A span is eligible for compression if all the following conditions are met 64 // 1. It's an exit span 65 // 2. The trace context has not been propagated to a downstream service 66 // 3. If the span has outcome (i.e., outcome is present and it's not null) then 67 // it should be success. It means spans with outcome indicating an issue of 68 // potential interest should not be compressed. 69 // 70 // The second condition is important so that we don't remove (compress) a span 71 // that may be the parent of a downstream service. This would orphan the sub- 72 // graph started by the downstream service and cause it to not appear in the 73 // waterfall view. 74 func (s *Span) compress(sibling *Span) bool { 75 // If the spans aren't siblings, we cannot compress them. 76 if s.parentID != sibling.parentID { 77 return false 78 } 79 80 strategy := s.canCompressComposite(sibling) 81 if strategy == 0 { 82 strategy = s.canCompressStandard(sibling) 83 } 84 85 // If the span cannot be compressed using any strategy. 86 if strategy == 0 { 87 return false 88 } 89 90 if s.composite.empty() { 91 s.composite = compositeSpan{ 92 count: 1, 93 sum: s.Duration, 94 compressionStrategy: strategy, 95 } 96 } 97 98 s.composite.count++ 99 s.composite.sum += sibling.Duration 100 siblingTimestamp := sibling.timestamp.Add(sibling.Duration) 101 if siblingTimestamp.After(s.composite.lastSiblingEndTime) { 102 s.composite.lastSiblingEndTime = siblingTimestamp 103 } 104 return true 105 } 106 107 // 108 // Span // 109 // 110 111 // attemptCompress tries to compress a span into a "composite span" when: 112 // 1. Compression is enabled on agent. 113 // 2. The cached span and the incoming span share the same parent (are siblings). 114 // 3. The cached span and the incoming span are consecutive spans. 115 // 4. The cached span and the incoming span are both exit spans, 116 // outcome == success and are short enough (See 117 // `ELASTIC_APM_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION` and 118 // `ELASTIC_APM_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION` for more info). 119 // 5. The cached span and the incoming span represent the same exact operation 120 // or the same kind of operation: 121 // - Are an exact match (same name, kind and destination service). 122 // - Are the same kind match (same kind and destination service). 123 // 124 // When a span has already been compressed using a particular strategy, it 125 // CANNOT continue to compress spans using a different strategy. 126 // 127 // The compression algorithm is fairly simple and only compresses spans into a 128 // composite span when the conditions listed above are met for all consecutive 129 // spans, at any point any span that doesn't meet the conditions, will cause 130 // the cache be evicted and the cached span will be returned. 131 // * When the incoming span is compressible, it will replace the cached span. 132 // * When the incoming span is not compressible, it will be enqueued as well. 133 // 134 // Returns `true` when the span has been cached, thus the caller should not 135 // enqueue the span. When `false` is returned, the cache is evicted and the 136 // caller should enqueue the span. 137 // 138 // It needs to be called with s.mu, s.parent.mu, s.tx.TransactionData.mu and 139 // s.tx.mu.Rlock held. 140 func (s *Span) attemptCompress() (*Span, bool) { 141 // If the span has already been evicted from the cache, ask the caller to 142 // end it. 143 if !s.compressedSpan.options.enabled { 144 return nil, false 145 } 146 147 // When a parent span ends, flush its cache. 148 if cache := s.compressedSpan.evict(); cache != nil { 149 return cache, false 150 } 151 152 // There are two distinct places where the span can be cached; the parent 153 // span and the transaction. The algorithm prefers storing the cached spans 154 // in its parent, and if nil, it will use the transaction's cache. 155 if s.parent != nil { 156 if !s.parent.ended() { 157 return s.parent.compressedSpan.compressOrEvictCache(s) 158 } 159 return nil, false 160 } 161 162 if s.tx != nil { 163 if !s.tx.ended() { 164 return s.tx.compressedSpan.compressOrEvictCache(s) 165 } 166 } 167 return nil, false 168 } 169 170 func (s *Span) isCompressionEligible() bool { 171 if s == nil { 172 return false 173 } 174 ctxPropagated := atomic.LoadUint32(&s.ctxPropagated) == 1 175 return s.exit && !ctxPropagated && 176 (s.Outcome == "" || s.Outcome == "success") 177 } 178 179 func (s *Span) canCompressStandard(sibling *Span) int { 180 if !s.isSameKind(sibling) { 181 return 0 182 } 183 184 // We've already established the spans are the same kind. 185 strategy := compressedStrategySameKind 186 maxDuration := s.compressedSpan.options.sameKindMaxDuration 187 188 // If it's an exact match, we then switch the settings 189 if s.isExactMatch(sibling) { 190 maxDuration = s.compressedSpan.options.exactMatchMaxDuration 191 strategy = compressedStrategyExactMatch 192 } 193 194 // Any spans that go over the maximum duration cannot be compressed. 195 if !s.durationLowerOrEq(sibling, maxDuration) { 196 return 0 197 } 198 199 // If the composite span already has a compression strategy it differs from 200 // the chosen strategy, the spans cannot be compressed. 201 if !s.composite.empty() && s.composite.compressionStrategy != strategy { 202 return 0 203 } 204 205 // Return whichever strategy was chosen. 206 return strategy 207 } 208 209 func (s *Span) canCompressComposite(sibling *Span) int { 210 if s.composite.empty() { 211 return 0 212 } 213 switch s.composite.compressionStrategy { 214 case compressedStrategyExactMatch: 215 if s.isExactMatch(sibling) && s.durationLowerOrEq(sibling, 216 s.compressedSpan.options.exactMatchMaxDuration, 217 ) { 218 return compressedStrategyExactMatch 219 } 220 case compressedStrategySameKind: 221 if s.isSameKind(sibling) && s.durationLowerOrEq(sibling, 222 s.compressedSpan.options.sameKindMaxDuration, 223 ) { 224 return compressedStrategySameKind 225 } 226 } 227 return 0 228 } 229 230 func (s *Span) durationLowerOrEq(sibling *Span, max time.Duration) bool { 231 return s.Duration <= max && sibling.Duration <= max 232 } 233 234 // 235 // SpanData // 236 // 237 238 // isExactMatch is used for compression purposes, two spans are considered an 239 // exact match if the have the same name and are of the same kind (see 240 // isSameKind for more details). 241 func (s *SpanData) isExactMatch(span *Span) bool { 242 return s.Name == span.Name && s.isSameKind(span) 243 } 244 245 // isSameKind is used for compression purposes, two spans are considered to be 246 // of the same kind if they have the same values for type, subtype, and 247 // `destination.service.resource`. 248 func (s *SpanData) isSameKind(span *Span) bool { 249 sameType := s.Type == span.Type 250 sameSubType := s.Subtype == span.Subtype 251 dstServiceTarget := s.Context.service.Target 252 otherDstServiceTarget := span.Context.service.Target 253 sameServiceTarget := dstServiceTarget != nil && otherDstServiceTarget != nil && 254 dstServiceTarget.Type == otherDstServiceTarget.Type && 255 dstServiceTarget.Name == otherDstServiceTarget.Name 256 257 return sameType && sameSubType && sameServiceTarget 258 } 259 260 // setCompressedSpanName changes the span name to "Calls to <destination service>" 261 // for composite spans that are compressed with the `"same_kind"` strategy. 262 func (s *SpanData) setCompressedSpanName() { 263 if s.composite.compressionStrategy != compressedStrategySameKind { 264 return 265 } 266 s.Name = s.getCompositeSpanName() 267 } 268 269 func (s *SpanData) getCompositeSpanName() string { 270 if s.Context.serviceTarget.Type == "" { 271 if s.Context.serviceTarget.Name == "" { 272 return compressedSpanSameKindName + "unknown" 273 } else { 274 return compressedSpanSameKindName + s.Context.serviceTarget.Name 275 } 276 } else if s.Context.serviceTarget.Name == "" { 277 return compressedSpanSameKindName + s.Context.serviceTarget.Type 278 } else { 279 return compressedSpanSameKindName + s.Context.serviceTarget.Type + "/" + s.Context.serviceTarget.Name 280 } 281 } 282 283 type compressedSpan struct { 284 cache *Span 285 options compressionOptions 286 } 287 288 // evict resets the cache to nil and returns the cached span after adjusting 289 // its Name, Duration, and timers. 290 // 291 // Should be only be called from Transaction.End() and Span.End(). 292 func (cs *compressedSpan) evict() *Span { 293 if cs.cache == nil { 294 return nil 295 } 296 cached := cs.cache 297 cs.cache = nil 298 // When the span composite is not empty, we need to adjust the duration just 299 // before it is reported and no more spans will be compressed into the 300 // composite. If this is done before ending the span, the duration of the span 301 // could potentially grow over the compressable threshold and result in 302 // compressable span not being compressed and reported separately. 303 if !cached.composite.empty() { 304 cached.Duration = cached.composite.lastSiblingEndTime.Sub(cached.timestamp) 305 cached.setCompressedSpanName() 306 } 307 return cached 308 } 309 310 func (cs *compressedSpan) compressOrEvictCache(s *Span) (*Span, bool) { 311 if !s.isCompressionEligible() { 312 return cs.evict(), false 313 } 314 315 if cs.cache == nil { 316 cs.cache = s 317 return nil, true 318 } 319 320 var evictedSpan *Span 321 if cs.cache.compress(s) { 322 // Since span has been compressed into the composite, we decrease the 323 // s.tx.spansCreated since the span has been compressed into a composite. 324 if s.tx != nil { 325 if !s.tx.ended() { 326 s.tx.spansCreated-- 327 } 328 } 329 } else { 330 evictedSpan = cs.evict() 331 cs.cache = s 332 } 333 return evictedSpan, true 334 }