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 }