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  }