Skip to content

Commit 4fae17e

Browse files
authored
Set default outcome according to spec (#1160)
Apart from HTTP and gRPC, which set outcome based on the status code, set the default outcome to "success" by default, or "failure" if any errors were captured within the transaction or span.
1 parent 7702edf commit 4fae17e

File tree

7 files changed

+124
-5
lines changed

7 files changed

+124
-5
lines changed

CHANGELOG.asciidoc

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ https://github.com/elastic/apm-agent-go/compare/v1.14.0...master[View commits]
3030
- module/apmprometheus: add support for mapping prometheus histograms. {pull}1145[#(1145)]
3131
- Fixed a bug where errors in cloud metadata discovery could lead to the process aborting during initialisation {pull}1158[#(1158)]
3232
- Fixed a data race related to HTTP request header sanitisation {pull}1159[#(1159)]
33+
- `apm.CaptureError`, `apm.Error.SetTransaction`, and `apm.Error.SetSpan` will now set the associated transaction or span's default outcome to "failure" {pull}1160[#(1160)]
3334
3435
[[release-notes-1.x]]
3536
=== Go Agent version 1.x

error.go

+17
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ func (e *Error) Error() string {
247247
//
248248
// If any custom context has been recorded in tx, it will also be carried across
249249
// to e, but will not override any custom context already recorded on e.
250+
//
251+
// SetTransaction also has the effect of setting tx's default outcome to "failure",
252+
// unless it is explicitly specified by the instrumentation.
250253
func (e *Error) SetTransaction(tx *Transaction) {
251254
tx.mu.RLock()
252255
traceContext := tx.traceContext
@@ -255,6 +258,9 @@ func (e *Error) SetTransaction(tx *Transaction) {
255258
if !tx.ended() {
256259
txType = tx.Type
257260
custom = tx.Context.model.Custom
261+
tx.TransactionData.mu.Lock()
262+
tx.TransactionData.errorCaptured = true
263+
tx.TransactionData.mu.Unlock()
258264
}
259265
tx.mu.RUnlock()
260266
e.setSpanData(traceContext, traceContext.Span, txType, custom)
@@ -277,8 +283,19 @@ func (e *Error) SetSpan(s *Span) {
277283
if !s.tx.ended() {
278284
txType = s.tx.Type
279285
custom = s.tx.Context.model.Custom
286+
s.tx.TransactionData.mu.Lock()
287+
s.tx.TransactionData.errorCaptured = true
288+
s.tx.TransactionData.mu.Unlock()
280289
}
281290
s.tx.mu.RUnlock()
291+
292+
s.mu.RLock()
293+
if !s.ended() {
294+
s.SpanData.mu.Lock()
295+
s.SpanData.errorCaptured = true
296+
s.SpanData.mu.Unlock()
297+
}
298+
s.mu.RUnlock()
282299
}
283300
e.setSpanData(s.traceContext, s.transactionID, txType, custom)
284301
}

internal/apmgodog/suitecontext_test.go

+20-4
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,26 @@ func (c *featureContext) initScenario(s *godog.ScenarioContext) {
144144
s.Step("^user sets transaction outcome to '(.*)'$", c.userSetsTransactionOutcome)
145145
s.Step("^span terminates with outcome '(.*)'$", c.spanTerminatesWithOutcome)
146146
s.Step("^transaction terminates with outcome '(.*)'$", c.transactionTerminatesWithOutcome)
147-
s.Step("^span terminates with an error$", func() error { return c.spanTerminatesWithOutcome("failure") })
148-
s.Step("^span terminates without error$", func() error { return c.spanTerminatesWithOutcome("success") })
149-
s.Step("^transaction terminates with an error$", func() error { return c.transactionTerminatesWithOutcome("failure") })
150-
s.Step("^transaction terminates without error$", func() error { return c.transactionTerminatesWithOutcome("success") })
147+
s.Step("^span terminates with an error$", func() error {
148+
e := c.tracer.NewError(errors.New("an error"))
149+
e.SetSpan(c.span)
150+
c.span.End()
151+
return nil
152+
})
153+
s.Step("^span terminates without error$", func() error {
154+
c.span.End()
155+
return nil
156+
})
157+
s.Step("^transaction terminates with an error$", func() error {
158+
e := c.tracer.NewError(errors.New("an error"))
159+
e.SetTransaction(c.transaction)
160+
c.transaction.End()
161+
return nil
162+
})
163+
s.Step("^transaction terminates without error$", func() error {
164+
c.transaction.End()
165+
return nil
166+
})
151167
s.Step("^span outcome is '(.*)'$", c.spanOutcomeIs)
152168
s.Step("^span outcome is \"(.*)\"$", c.spanOutcomeIs)
153169
s.Step("^transaction outcome is '(.*)'$", c.transactionOutcomeIs)

span.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ func (s *Span) SetStacktrace(skip int) {
293293
if s.ended() {
294294
return
295295
}
296+
s.SpanData.mu.Lock()
297+
defer s.SpanData.mu.Unlock()
296298
s.SpanData.setStacktrace(skip + 1)
297299
}
298300

@@ -346,6 +348,13 @@ func (s *Span) End() {
346348
}
347349
if s.Outcome == "" {
348350
s.Outcome = s.Context.outcome()
351+
if s.Outcome == "" {
352+
if s.errorCaptured {
353+
s.Outcome = "failure"
354+
} else {
355+
s.Outcome = "success"
356+
}
357+
}
349358
}
350359
if !s.dropped() && len(s.stacktrace) == 0 &&
351360
s.Duration >= s.stackFramesMinDuration {
@@ -561,7 +570,9 @@ type SpanData struct {
561570
// Context describes the context in which span occurs.
562571
Context SpanContext
563572

564-
stacktrace []stacktrace.Frame
573+
mu sync.Mutex
574+
stacktrace []stacktrace.Frame
575+
errorCaptured bool
565576
}
566577

567578
func (s *SpanData) setStacktrace(skip int) {

span_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"testing"
2727
"time"
2828

29+
"github.com/pkg/errors"
2930
"github.com/stretchr/testify/assert"
3031
"github.com/stretchr/testify/require"
3132

@@ -1263,3 +1264,33 @@ func TestSpanFastExitNoTransaction(t *testing.T) {
12631264
Dropped: 0,
12641265
}, transaction.SpanCount)
12651266
}
1267+
1268+
func TestSpanOutcome(t *testing.T) {
1269+
_, spans, _ := apmtest.WithTransaction(func(ctx context.Context) {
1270+
span1, _ := apm.StartSpan(ctx, "name", "type")
1271+
span1.End()
1272+
1273+
span2, _ := apm.StartSpan(ctx, "name", "type")
1274+
span2.Outcome = "unknown"
1275+
span2.End()
1276+
1277+
span3, _ := apm.StartSpan(ctx, "name", "type")
1278+
span3.Context.SetHTTPStatusCode(200)
1279+
span3.End()
1280+
1281+
span4, _ := apm.StartSpan(ctx, "name", "type")
1282+
span4.Context.SetHTTPStatusCode(400)
1283+
span4.End()
1284+
1285+
span5, ctx := apm.StartSpan(ctx, "name", "type")
1286+
apm.CaptureError(ctx, errors.New("an error")).Send()
1287+
span5.End()
1288+
})
1289+
1290+
require.Len(t, spans, 5)
1291+
assert.Equal(t, "success", spans[0].Outcome) // default
1292+
assert.Equal(t, "unknown", spans[1].Outcome) // specified
1293+
assert.Equal(t, "success", spans[2].Outcome) // HTTP status < 400
1294+
assert.Equal(t, "failure", spans[3].Outcome) // HTTP status >= 400
1295+
assert.Equal(t, "failure", spans[4].Outcome)
1296+
}

transaction.go

+8
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,13 @@ func (tx *Transaction) End() {
287287
}
288288
if tx.Outcome == "" {
289289
tx.Outcome = tx.Context.outcome()
290+
if tx.Outcome == "" {
291+
if tx.errorCaptured {
292+
tx.Outcome = "failure"
293+
} else {
294+
tx.Outcome = "success"
295+
}
296+
}
290297
}
291298
// Hold the transaction data lock to check if the transaction has any
292299
// compressed spans in its cache, if so, evict cache and end the span.
@@ -370,6 +377,7 @@ type TransactionData struct {
370377
timestamp time.Time
371378

372379
mu sync.Mutex
380+
errorCaptured bool
373381
spansCreated int
374382
spansDropped int
375383
childrenTimer childrenTimer

transaction_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"testing"
2727
"time"
2828

29+
"github.com/pkg/errors"
2930
"github.com/stretchr/testify/assert"
3031
"github.com/stretchr/testify/require"
3132

@@ -513,6 +514,40 @@ func TestTransactionDroppedSpansStats(t *testing.T) {
513514
})
514515
}
515516

517+
func TestTransactionOutcome(t *testing.T) {
518+
tracer := apmtest.NewRecordingTracer()
519+
defer tracer.Close()
520+
521+
tx1 := tracer.StartTransaction("name", "type")
522+
tx1.End()
523+
524+
tx2 := tracer.StartTransaction("name", "type")
525+
tx2.Outcome = "unknown"
526+
tx2.End()
527+
528+
tx3 := tracer.StartTransaction("name", "type")
529+
tx3.Context.SetHTTPStatusCode(400)
530+
tx3.End()
531+
532+
tx4 := tracer.StartTransaction("name", "type")
533+
tx4.Context.SetHTTPStatusCode(500)
534+
tx4.End()
535+
536+
tx5 := tracer.StartTransaction("name", "type")
537+
ctx := apm.ContextWithTransaction(context.Background(), tx5)
538+
apm.CaptureError(ctx, errors.New("an error")).Send()
539+
tx5.End()
540+
541+
tracer.Flush(nil)
542+
transactions := tracer.Payloads().Transactions
543+
require.Len(t, transactions, 5)
544+
assert.Equal(t, "success", transactions[0].Outcome) // default
545+
assert.Equal(t, "unknown", transactions[1].Outcome) // specified
546+
assert.Equal(t, "success", transactions[2].Outcome) // HTTP status < 500
547+
assert.Equal(t, "failure", transactions[3].Outcome) // HTTP status >= 500
548+
assert.Equal(t, "failure", transactions[4].Outcome)
549+
}
550+
516551
func BenchmarkTransaction(b *testing.B) {
517552
tracer, err := apm.NewTracer("service", "")
518553
require.NoError(b, err)

0 commit comments

Comments
 (0)