github.com/waldiirawan/apm-agent-go/v2@v2.2.2/transaction_test.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_test
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"math/rand"
    24  	"os"
    25  	"sync"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/pkg/errors"
    30  	"github.com/stretchr/testify/assert"
    31  	"github.com/stretchr/testify/require"
    32  
    33  	"github.com/waldiirawan/apm-agent-go/v2"
    34  	"github.com/waldiirawan/apm-agent-go/v2/apmtest"
    35  	"github.com/waldiirawan/apm-agent-go/v2/model"
    36  	"github.com/waldiirawan/apm-agent-go/v2/transport/transporttest"
    37  )
    38  
    39  func TestStartTransactionTraceContextOptions(t *testing.T) {
    40  	testStartTransactionTraceContextOptions(t, false)
    41  	testStartTransactionTraceContextOptions(t, true)
    42  }
    43  
    44  func testStartTransactionTraceContextOptions(t *testing.T, recorded bool) {
    45  	tracer, _ := transporttest.NewRecorderTracer()
    46  	defer tracer.Close()
    47  	tracer.SetSampler(samplerFunc(func(apm.SampleParams) apm.SampleResult {
    48  		panic("nope")
    49  	}))
    50  
    51  	opts := apm.TransactionOptions{
    52  		TraceContext: apm.TraceContext{
    53  			Trace: apm.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
    54  			Span:  apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
    55  		},
    56  	}
    57  	opts.TraceContext.Options = opts.TraceContext.Options.WithRecorded(recorded)
    58  
    59  	tx := tracer.StartTransactionOptions("name", "type", opts)
    60  	result := tx.TraceContext()
    61  	assert.Equal(t, recorded, result.Options.Recorded())
    62  	tx.Discard()
    63  }
    64  
    65  func TestStartTransactionInvalidTraceContext(t *testing.T) {
    66  	startTransactionInvalidTraceContext(t, apm.TraceContext{
    67  		// Trace is all zeroes, which is invalid.
    68  		Span: apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
    69  	})
    70  }
    71  
    72  func startTransactionInvalidTraceContext(t *testing.T, traceContext apm.TraceContext) {
    73  	tracer, _ := transporttest.NewRecorderTracer()
    74  	defer tracer.Close()
    75  
    76  	var samplerCalled bool
    77  	tracer.SetSampler(samplerFunc(func(apm.SampleParams) apm.SampleResult {
    78  		samplerCalled = true
    79  		return apm.SampleResult{Sampled: true}
    80  	}))
    81  
    82  	opts := apm.TransactionOptions{TraceContext: traceContext}
    83  	tx := tracer.StartTransactionOptions("name", "type", opts)
    84  	assert.True(t, samplerCalled)
    85  	tx.Discard()
    86  }
    87  
    88  func TestContinuationStrategy(t *testing.T) {
    89  	testCases := map[string]struct {
    90  		traceContext     apm.TraceContext
    91  		strategy         string
    92  		expectNewTraceID bool
    93  		expectSpanLink   bool
    94  	}{
    95  		"restart": {
    96  			traceContext: apm.TraceContext{
    97  				Trace: apm.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
    98  				Span:  apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
    99  			},
   100  			strategy:         "restart",
   101  			expectNewTraceID: true,
   102  			expectSpanLink:   true,
   103  		},
   104  		"restart with es": {
   105  			traceContext: apm.TraceContext{
   106  				Trace: apm.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
   107  				Span:  apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
   108  				State: apm.NewTraceState(apm.TraceStateEntry{Key: "es", Value: "s:0.5"}),
   109  			},
   110  			strategy:         "restart",
   111  			expectNewTraceID: true,
   112  			expectSpanLink:   true,
   113  		},
   114  		"continue": {
   115  			traceContext: apm.TraceContext{
   116  				Trace: apm.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
   117  				Span:  apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
   118  			},
   119  			strategy:         "continue",
   120  			expectNewTraceID: false,
   121  			expectSpanLink:   false,
   122  		},
   123  		"restart_external": {
   124  			traceContext: apm.TraceContext{
   125  				Trace: apm.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
   126  				Span:  apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
   127  			},
   128  			strategy:         "restart_external",
   129  			expectNewTraceID: true,
   130  			expectSpanLink:   true,
   131  		},
   132  		"restart_external with missing header": {
   133  			traceContext:     apm.TraceContext{},
   134  			strategy:         "restart_external",
   135  			expectNewTraceID: true,
   136  			expectSpanLink:   false,
   137  		},
   138  		"restart_external with es": {
   139  			traceContext: apm.TraceContext{
   140  				Trace: apm.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
   141  				Span:  apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
   142  				State: apm.NewTraceState(apm.TraceStateEntry{Key: "es", Value: "s:0.5"}),
   143  			},
   144  			strategy:         "restart_external",
   145  			expectNewTraceID: false,
   146  			expectSpanLink:   false,
   147  		},
   148  	}
   149  	for name, tc := range testCases {
   150  		t.Run(name, func(t *testing.T) {
   151  			tracer, transport := transporttest.NewRecorderTracer()
   152  			defer tracer.Close()
   153  
   154  			tracer.SetContinuationStrategy(tc.strategy)
   155  
   156  			providedSpanID := model.SpanID(tc.traceContext.Span)
   157  			providedTraceID := model.TraceID(tc.traceContext.Trace)
   158  
   159  			tx := tracer.StartTransactionOptions("name", "type", apm.TransactionOptions{
   160  				TraceContext: tc.traceContext,
   161  			})
   162  			tx.End()
   163  
   164  			tracer.Flush(nil)
   165  			payloads := transport.Payloads()
   166  
   167  			require.Len(t, payloads.Transactions, 1)
   168  
   169  			tr := payloads.Transactions[0]
   170  			assert.NotZero(t, tr.ID)
   171  
   172  			if tc.expectNewTraceID {
   173  				assert.NotEqual(t, providedTraceID, tr.TraceID)
   174  			} else {
   175  				assert.Equal(t, providedTraceID, tr.TraceID)
   176  			}
   177  
   178  			if tc.expectSpanLink {
   179  				assert.Len(t, tr.Links, 1)
   180  				link := tr.Links[0]
   181  				assert.Equal(t, providedTraceID, link.TraceID)
   182  				assert.Equal(t, providedSpanID, link.SpanID)
   183  			} else {
   184  				assert.Empty(t, tr.Links)
   185  			}
   186  		})
   187  	}
   188  }
   189  
   190  func TestStartTransactionTraceParentSpanIDSpecified(t *testing.T) {
   191  	startTransactionIDSpecified(t, apm.TraceContext{
   192  		Trace: apm.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
   193  		Span:  apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
   194  	})
   195  }
   196  
   197  func TestStartTransactionTraceIDSpecified(t *testing.T) {
   198  	startTransactionIDSpecified(t, apm.TraceContext{
   199  		Trace: apm.TraceID{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
   200  	})
   201  }
   202  
   203  func TestStartTransactionIDSpecified(t *testing.T) {
   204  	startTransactionIDSpecified(t, apm.TraceContext{})
   205  }
   206  
   207  func startTransactionIDSpecified(t *testing.T, traceContext apm.TraceContext) {
   208  	tracer, _ := transporttest.NewRecorderTracer()
   209  	defer tracer.Close()
   210  
   211  	opts := apm.TransactionOptions{
   212  		TraceContext:  traceContext,
   213  		TransactionID: apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
   214  	}
   215  	tx := tracer.StartTransactionOptions("name", "type", opts)
   216  	assert.Equal(t, opts.TransactionID, tx.TraceContext().Span)
   217  	tx.Discard()
   218  }
   219  
   220  func TestTransactionEnsureParent(t *testing.T) {
   221  	tracer, transport := transporttest.NewRecorderTracer()
   222  	defer tracer.Close()
   223  
   224  	tx := tracer.StartTransaction("name", "type")
   225  	traceContext := tx.TraceContext()
   226  
   227  	parentSpan := tx.EnsureParent()
   228  	assert.NotZero(t, parentSpan)
   229  	assert.NotEqual(t, traceContext.Span, parentSpan)
   230  
   231  	// EnsureParent is idempotent.
   232  	parentSpan2 := tx.EnsureParent()
   233  	assert.Equal(t, parentSpan, parentSpan2)
   234  
   235  	tx.End()
   236  
   237  	// For an ended transaction, EnsureParent will return a zero value
   238  	// even if the transaction had a parent at the time it was ended.
   239  	parentSpan3 := tx.EnsureParent()
   240  	assert.Zero(t, parentSpan3)
   241  
   242  	tracer.Flush(nil)
   243  	payloads := transport.Payloads()
   244  	require.Len(t, payloads.Transactions, 1)
   245  	assert.Equal(t, model.SpanID(parentSpan), payloads.Transactions[0].ParentID)
   246  }
   247  
   248  func TestTransactionEnsureType(t *testing.T) {
   249  	tracer, transport := transporttest.NewRecorderTracer()
   250  	defer tracer.Close()
   251  
   252  	tx := tracer.StartTransaction("name", "")
   253  	tx.End()
   254  	tracer.Flush(nil)
   255  
   256  	payloads := transport.Payloads()
   257  	require.Len(t, payloads.Transactions, 1)
   258  	assert.Equal(t, "custom", payloads.Transactions[0].Type)
   259  }
   260  
   261  func TestTransactionParentID(t *testing.T) {
   262  	tracer := apmtest.NewRecordingTracer()
   263  	defer tracer.Close()
   264  
   265  	tx := tracer.StartTransaction("name", "type")
   266  	traceContext := tx.TraceContext()
   267  
   268  	// root tx has no parent.
   269  	parentSpan := tx.ParentID()
   270  	assert.Zero(t, parentSpan)
   271  
   272  	// Create a child transaction with the TraceContext from the parent.
   273  	txChild := tracer.StartTransactionOptions("child", "type", apm.TransactionOptions{
   274  		TraceContext: tx.TraceContext(),
   275  	})
   276  
   277  	// Assert that the Parent ID isn't zero and matches the parent ID.
   278  	// Parent TX ID
   279  	parentTxID := traceContext.Span
   280  	childSpanParentID := txChild.ParentID()
   281  	assert.NotZero(t, childSpanParentID)
   282  	assert.Equal(t, parentTxID, childSpanParentID)
   283  
   284  	txChild.End()
   285  	tx.End()
   286  
   287  	// Assert that we can obtain the parent ID even after the transaction
   288  	// has ended.
   289  	assert.NotZero(t, txChild.ParentID())
   290  
   291  	tracer.Flush(nil)
   292  	payloads := tracer.Payloads()
   293  	require.Len(t, payloads.Transactions, 2)
   294  
   295  	// First recorded transaction Parent ID matches the child's Parent ID
   296  	assert.Equal(t, model.SpanID(childSpanParentID), payloads.Transactions[0].ParentID)
   297  	// First recorded transaction Parent ID matches the parent transaction ID.
   298  	assert.Equal(t, model.SpanID(parentTxID), payloads.Transactions[0].ParentID)
   299  
   300  	// Parent transaction has a zero ParentID.
   301  	assert.Zero(t, payloads.Transactions[1].ParentID)
   302  }
   303  
   304  func TestTransactionParentIDWithEnsureParent(t *testing.T) {
   305  	tracer := apmtest.NewRecordingTracer()
   306  	defer tracer.Close()
   307  
   308  	tx := tracer.StartTransaction("name", "type")
   309  
   310  	rootParentIDEmpty := tx.ParentID()
   311  	assert.Zero(t, rootParentIDEmpty)
   312  
   313  	ensureParentResult := tx.EnsureParent()
   314  	assert.NotZero(t, ensureParentResult)
   315  
   316  	rootParentIDNotEmpty := tx.ParentID()
   317  	assert.Equal(t, ensureParentResult, rootParentIDNotEmpty)
   318  
   319  	tx.End()
   320  
   321  	tracer.Flush(nil)
   322  	payloads := tracer.Payloads()
   323  	require.Len(t, payloads.Transactions, 1)
   324  
   325  	assert.Equal(t, model.SpanID(rootParentIDNotEmpty), payloads.Transactions[0].ParentID)
   326  }
   327  
   328  func TestTransactionContextNotSampled(t *testing.T) {
   329  	tracer := apmtest.NewRecordingTracer()
   330  	defer tracer.Close()
   331  	tracer.SetSampler(samplerFunc(func(apm.SampleParams) apm.SampleResult {
   332  		return apm.SampleResult{Sampled: false}
   333  	}))
   334  
   335  	tx := tracer.StartTransaction("name", "type")
   336  	tx.Context.SetLabel("foo", "bar")
   337  	tx.End()
   338  	tracer.Flush(nil)
   339  
   340  	payloads := tracer.Payloads()
   341  	require.Len(t, payloads.Transactions, 1)
   342  	assert.Nil(t, payloads.Transactions[0].Context)
   343  }
   344  
   345  func TestTransactionNotRecording(t *testing.T) {
   346  	tracer := apmtest.NewRecordingTracer()
   347  	defer tracer.Close()
   348  	tracer.SetRecording(false)
   349  	tracer.SetSampler(samplerFunc(func(apm.SampleParams) apm.SampleResult {
   350  		panic("should not be called")
   351  	}))
   352  
   353  	tx := tracer.StartTransaction("name", "type")
   354  	require.NotNil(t, tx)
   355  	require.NotNil(t, tx.TransactionData)
   356  	tx.End()
   357  	require.Nil(t, tx.TransactionData)
   358  	tracer.Flush(nil)
   359  
   360  	payloads := tracer.Payloads()
   361  	require.Empty(t, payloads.Transactions)
   362  }
   363  
   364  func TestTransactionSampleRate(t *testing.T) {
   365  	type test struct {
   366  		actualSampleRate   float64
   367  		recordedSampleRate float64
   368  		expectedTraceState string
   369  	}
   370  	tests := []test{
   371  		{0, 0, "es=s:0"},
   372  		{1, 1, "es=s:1"},
   373  		{0.00001, 0.0001, "es=s:0.0001"},
   374  		{0.55554, 0.5555, "es=s:0.5555"},
   375  		{0.55555, 0.5556, "es=s:0.5556"},
   376  		{0.55556, 0.5556, "es=s:0.5556"},
   377  	}
   378  	for _, test := range tests {
   379  		test := test // copy for closure
   380  		t.Run(fmt.Sprintf("%v", test.actualSampleRate), func(t *testing.T) {
   381  			tracer := apmtest.NewRecordingTracer()
   382  			defer tracer.Close()
   383  
   384  			tracer.SetSampler(apm.NewRatioSampler(test.actualSampleRate))
   385  			tx := tracer.StartTransactionOptions("name", "type", apm.TransactionOptions{
   386  				// Use a known transaction ID for deterministic sampling.
   387  				TransactionID: apm.SpanID{0, 1, 2, 3, 4, 5, 6, 7},
   388  			})
   389  			tx.End()
   390  			tracer.Flush(nil)
   391  
   392  			payloads := tracer.Payloads()
   393  			assert.Equal(t, test.recordedSampleRate, *payloads.Transactions[0].SampleRate)
   394  			assert.Equal(t, test.expectedTraceState, tx.TraceContext().State.String())
   395  		})
   396  	}
   397  }
   398  
   399  func TestTransactionUnsampledSampleRate(t *testing.T) {
   400  	tracer := apmtest.NewRecordingTracer()
   401  	defer tracer.Close()
   402  	tracer.SetSampler(apm.NewRatioSampler(0.5))
   403  
   404  	// Create transactions until we get an unsampled one.
   405  	//
   406  	// Even though the configured sampling rate is 0.5,
   407  	// we record sample_rate=0 to ensure the server does
   408  	// not count the transaction toward metrics.
   409  	var tx *apm.Transaction
   410  	for {
   411  		tx = tracer.StartTransactionOptions("name", "type", apm.TransactionOptions{})
   412  		if !tx.Sampled() {
   413  			tx.End()
   414  			break
   415  		}
   416  		tx.Discard()
   417  	}
   418  	tracer.Flush(nil)
   419  
   420  	payloads := tracer.Payloads()
   421  	assert.Equal(t, float64(0), *payloads.Transactions[0].SampleRate)
   422  	assert.Equal(t, "es=s:0", tx.TraceContext().State.String())
   423  }
   424  
   425  func TestTransactionSampleRatePropagation(t *testing.T) {
   426  	tracer := apmtest.NewRecordingTracer()
   427  	defer tracer.Close()
   428  
   429  	for _, tracestate := range []apm.TraceState{
   430  		apm.NewTraceState(apm.TraceStateEntry{Key: "es", Value: "s:0.5"}),
   431  		apm.NewTraceState(apm.TraceStateEntry{Key: "es", Value: "x:y;s:0.5;zz:y"}),
   432  		apm.NewTraceState(
   433  			apm.TraceStateEntry{Key: "other", Value: "s:1.0"},
   434  			apm.TraceStateEntry{Key: "es", Value: "s:0.5"},
   435  		),
   436  	} {
   437  		tx := tracer.StartTransactionOptions("name", "type", apm.TransactionOptions{
   438  			TraceContext: apm.TraceContext{
   439  				Trace: apm.TraceID{1},
   440  				Span:  apm.SpanID{1},
   441  				State: tracestate,
   442  			},
   443  		})
   444  		tx.End()
   445  	}
   446  	tracer.Flush(nil)
   447  
   448  	payloads := tracer.Payloads()
   449  	assert.Len(t, payloads.Transactions, 3)
   450  	for _, tx := range payloads.Transactions {
   451  		assert.Equal(t, 0.5, *tx.SampleRate)
   452  	}
   453  }
   454  
   455  func TestTransactionSampleRateOmission(t *testing.T) {
   456  	tracer := apmtest.NewRecordingTracer()
   457  	defer tracer.Close()
   458  
   459  	// For downstream transactions, sample_rate should be
   460  	// omitted if a valid value is not found in tracestate.
   461  	for _, tracestate := range []apm.TraceState{
   462  		apm.TraceState{}, // empty
   463  		apm.NewTraceState(apm.TraceStateEntry{Key: "other", Value: "s:1.0"}), // not "es", ignored
   464  		apm.NewTraceState(apm.TraceStateEntry{Key: "es", Value: "s:123.0"}),  // out of range
   465  		apm.NewTraceState(apm.TraceStateEntry{Key: "es", Value: ""}),         // 's' missing
   466  		apm.NewTraceState(apm.TraceStateEntry{Key: "es", Value: "wat"}),      // malformed
   467  	} {
   468  		for _, sampled := range []bool{false, true} {
   469  			tx := tracer.StartTransactionOptions("name", "type", apm.TransactionOptions{
   470  				TraceContext: apm.TraceContext{
   471  					Trace:   apm.TraceID{1},
   472  					Span:    apm.SpanID{1},
   473  					Options: apm.TraceOptions(0).WithRecorded(sampled),
   474  					State:   tracestate,
   475  				},
   476  			})
   477  			tx.End()
   478  		}
   479  	}
   480  	tracer.Flush(nil)
   481  
   482  	payloads := tracer.Payloads()
   483  	assert.Len(t, payloads.Transactions, 10)
   484  	for _, tx := range payloads.Transactions {
   485  		assert.Nil(t, tx.SampleRate)
   486  	}
   487  }
   488  
   489  func TestTransactionSpanLink(t *testing.T) {
   490  	tracer := apmtest.NewRecordingTracer()
   491  	defer tracer.Close()
   492  
   493  	links := []apm.SpanLink{
   494  		{Trace: apm.TraceID{1}, Span: apm.SpanID{1}},
   495  		{Trace: apm.TraceID{2}, Span: apm.SpanID{2}},
   496  	}
   497  
   498  	tx := tracer.StartTransactionOptions("name", "type", apm.TransactionOptions{Links: links})
   499  	tx.End()
   500  
   501  	tracer.Flush(nil)
   502  
   503  	payloads := tracer.Payloads()
   504  	assert.Len(t, payloads.Transactions, 1)
   505  
   506  	// Assert span links are identical.
   507  	expectedLinks := []model.SpanLink{
   508  		{TraceID: model.TraceID{1}, SpanID: model.SpanID{1}},
   509  		{TraceID: model.TraceID{2}, SpanID: model.SpanID{2}},
   510  	}
   511  	assert.Equal(t, expectedLinks, payloads.Transactions[0].Links)
   512  }
   513  
   514  func TestTransactionDiscard(t *testing.T) {
   515  	tracer, transport := transporttest.NewRecorderTracer()
   516  	defer tracer.Close()
   517  
   518  	tx := tracer.StartTransaction("name", "type")
   519  	tx.Discard()
   520  	assert.Nil(t, tx.TransactionData)
   521  	tx.End() // ending after discarding should be a no-op
   522  
   523  	tracer.Flush(nil)
   524  	payloads := transport.Payloads()
   525  	require.Empty(t, payloads)
   526  }
   527  
   528  func TestTransactionDroppedSpansStats(t *testing.T) {
   529  	exitSpanOpts := apm.SpanOptions{ExitSpan: true}
   530  	generateSpans := func(ctx context.Context, spans int) {
   531  		for i := 0; i < spans; i++ {
   532  			span, _ := apm.StartSpanOptions(ctx,
   533  				fmt.Sprintf("GET %d", i),
   534  				fmt.Sprintf("request_%d", i),
   535  				exitSpanOpts,
   536  			)
   537  			span.Duration = 10 * time.Microsecond
   538  			span.End()
   539  		}
   540  	}
   541  	type extraSpan struct {
   542  		id, count int
   543  	}
   544  	generateExtraSpans := func(ctx context.Context, genExtra []extraSpan) {
   545  		for _, extra := range genExtra {
   546  			for i := 0; i < extra.count; i++ {
   547  				span, _ := apm.StartSpanOptions(ctx,
   548  					fmt.Sprintf("GET %d", extra.id),
   549  					fmt.Sprintf("request_%d", extra.id),
   550  					exitSpanOpts,
   551  				)
   552  				span.Duration = 10 * time.Microsecond
   553  				span.End()
   554  			}
   555  		}
   556  	}
   557  	// The default limit is 500 spans.
   558  	// The default exit_span_min_duration is `1ms`.
   559  	t.Run("DefaultLimit", func(t *testing.T) {
   560  		tracer := apmtest.NewRecordingTracer()
   561  		defer tracer.Close()
   562  		tracer.SetSpanCompressionEnabled(false)
   563  
   564  		tx, _, _ := tracer.WithTransaction(func(ctx context.Context) {
   565  			generateSpans(ctx, 1000)
   566  			generateExtraSpans(ctx, []extraSpan{
   567  				{count: 100, id: 501},
   568  				{count: 50, id: 600},
   569  			})
   570  		})
   571  		// Ensure that the extra spans we generated are aggregated
   572  		for _, span := range tx.DroppedSpansStats {
   573  			if span.ServiceTargetType == "request_501" {
   574  				assert.Equal(t, 101, span.Duration.Count)
   575  				assert.Equal(t, int64(1010), span.Duration.Sum.Us)
   576  			} else if span.ServiceTargetType == "request_600" {
   577  				assert.Equal(t, 51, span.Duration.Count)
   578  				assert.Equal(t, int64(510), span.Duration.Sum.Us)
   579  			} else {
   580  				assert.Equal(t, 1, span.Duration.Count)
   581  				assert.Equal(t, int64(10), span.Duration.Sum.Us)
   582  			}
   583  		}
   584  	})
   585  	t.Run("DefaultLimit/DropShortExitSpans", func(t *testing.T) {
   586  		tracer := apmtest.NewRecordingTracer()
   587  		defer tracer.Close()
   588  		// Set the exit span minimum duration. This test asserts that spans
   589  		// with a duration over the span minimum duration are not dropped.
   590  		tracer.SetSpanCompressionEnabled(false)
   591  		tracer.SetExitSpanMinDuration(time.Microsecond)
   592  
   593  		// Each of the generated spans duration is 10 microseconds.
   594  		tx, spans, _ := tracer.WithTransaction(func(ctx context.Context) {
   595  			generateSpans(ctx, 150)
   596  		})
   597  
   598  		require.Equal(t, 150, len(spans))
   599  		require.Equal(t, 0, len(tx.DroppedSpansStats))
   600  	})
   601  	t.Run("MaxSpans100", func(t *testing.T) {
   602  		tracer := apmtest.NewRecordingTracer()
   603  		defer tracer.Close()
   604  		// Assert that any spans over 100 are dropped and stats are aggregated.
   605  		tracer.SetSpanCompressionEnabled(false)
   606  		tracer.SetMaxSpans(100)
   607  
   608  		tx, spans, _ := tracer.WithTransaction(func(ctx context.Context) {
   609  			generateSpans(ctx, 300)
   610  			generateExtraSpans(ctx, []extraSpan{
   611  				{count: 50, id: 51},
   612  				{count: 20, id: 60},
   613  			})
   614  		})
   615  
   616  		require.Equal(t, 0, len(spans))
   617  		require.Equal(t, 128, len(tx.DroppedSpansStats))
   618  
   619  		for _, span := range tx.DroppedSpansStats {
   620  			if span.ServiceTargetType == "request_51" {
   621  				assert.Equal(t, 51, span.Duration.Count)
   622  				assert.Equal(t, int64(510), span.Duration.Sum.Us)
   623  			} else if span.ServiceTargetType == "request_60" {
   624  				assert.Equal(t, 21, span.Duration.Count)
   625  				assert.Equal(t, int64(210), span.Duration.Sum.Us)
   626  			} else {
   627  				assert.Equal(t, 1, span.Duration.Count)
   628  				assert.Equal(t, int64(10), span.Duration.Sum.Us)
   629  			}
   630  		}
   631  	})
   632  	t.Run("MaxSpans10WithDisabledBreakdownMetrics", func(t *testing.T) {
   633  		os.Setenv("ELASTIC_APM_BREAKDOWN_METRICS", "false")
   634  		defer os.Unsetenv("ELASTIC_APM_BREAKDOWN_METRICS")
   635  		tracer := apmtest.NewRecordingTracer()
   636  		defer tracer.Close()
   637  
   638  		// Assert that any spans over 10 are dropped and stats are aggregated.
   639  		tracer.SetMaxSpans(10)
   640  
   641  		// All spans except the one that we manually create will be dropped since
   642  		// their duration is lower than `exit_span_min_duration`.
   643  		tx, spans, _ := tracer.WithTransaction(func(ctx context.Context) {
   644  			span, _ := apm.StartSpanOptions(ctx, "name", "type", exitSpanOpts)
   645  			span.Duration = time.Second
   646  			span.End()
   647  			generateSpans(ctx, 50)
   648  		})
   649  
   650  		require.Len(t, spans, 1)
   651  		require.Len(t, tx.DroppedSpansStats, 50)
   652  
   653  		// Ensure that the extra spans we generated are aggregated
   654  		for _, span := range tx.DroppedSpansStats {
   655  			assert.Equal(t, 1, span.Duration.Count)
   656  			assert.Equal(t, int64(10), span.Duration.Sum.Us)
   657  		}
   658  	})
   659  }
   660  
   661  func TestTransactionOutcome(t *testing.T) {
   662  	tracer := apmtest.NewRecordingTracer()
   663  	defer tracer.Close()
   664  
   665  	tx1 := tracer.StartTransaction("name", "type")
   666  	tx1.End()
   667  
   668  	tx2 := tracer.StartTransaction("name", "type")
   669  	tx2.Outcome = "unknown"
   670  	tx2.End()
   671  
   672  	tx3 := tracer.StartTransaction("name", "type")
   673  	tx3.Context.SetHTTPStatusCode(400)
   674  	tx3.End()
   675  
   676  	tx4 := tracer.StartTransaction("name", "type")
   677  	tx4.Context.SetHTTPStatusCode(500)
   678  	tx4.End()
   679  
   680  	tx5 := tracer.StartTransaction("name", "type")
   681  	ctx := apm.ContextWithTransaction(context.Background(), tx5)
   682  	apm.CaptureError(ctx, errors.New("an error")).Send()
   683  	tx5.End()
   684  
   685  	tracer.Flush(nil)
   686  	transactions := tracer.Payloads().Transactions
   687  	require.Len(t, transactions, 5)
   688  	assert.Equal(t, "success", transactions[0].Outcome) // default
   689  	assert.Equal(t, "unknown", transactions[1].Outcome) // specified
   690  	assert.Equal(t, "success", transactions[2].Outcome) // HTTP status < 500
   691  	assert.Equal(t, "failure", transactions[3].Outcome) // HTTP status >= 500
   692  	assert.Equal(t, "failure", transactions[4].Outcome)
   693  }
   694  
   695  func BenchmarkTransaction(b *testing.B) {
   696  	tracer := apmtest.DiscardTracer
   697  
   698  	names := []string{}
   699  	for i := 0; i < 1000; i++ {
   700  		names = append(names, fmt.Sprintf("/some/route/%d", i))
   701  	}
   702  
   703  	var mu sync.Mutex
   704  	globalRand := rand.New(rand.NewSource(time.Now().UnixNano()))
   705  	b.ResetTimer()
   706  
   707  	b.RunParallel(func(pb *testing.PB) {
   708  		mu.Lock()
   709  		rand := rand.New(rand.NewSource(globalRand.Int63()))
   710  		mu.Unlock()
   711  		for pb.Next() {
   712  			tx := tracer.StartTransaction(names[rand.Intn(len(names))], "type")
   713  			tx.End()
   714  		}
   715  	})
   716  }
   717  
   718  type samplerFunc func(apm.SampleParams) apm.SampleResult
   719  
   720  func (f samplerFunc) Sample(p apm.SampleParams) apm.SampleResult {
   721  	return f(p)
   722  }