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 18package apm_test 19 20import ( 21 "context" 22 "fmt" 23 "net" 24 "os" 25 "path/filepath" 26 "reflect" 27 "runtime" 28 "strconv" 29 "syscall" 30 "testing" 31 32 "github.com/pkg/errors" 33 "github.com/stretchr/testify/assert" 34 "github.com/stretchr/testify/require" 35 36 "go.elastic.co/apm" 37 "go.elastic.co/apm/apmtest" 38 "go.elastic.co/apm/model" 39 "go.elastic.co/apm/stacktrace" 40 "go.elastic.co/apm/transport/transporttest" 41) 42 43func TestErrorID(t *testing.T) { 44 var errorID apm.ErrorID 45 _, _, errors := apmtest.WithTransaction(func(ctx context.Context) { 46 e := apm.CaptureError(ctx, errors.New("boom")) 47 errorID = e.ID 48 e.Send() 49 }) 50 require.Len(t, errors, 1) 51 assert.NotZero(t, errorID) 52 assert.Equal(t, model.TraceID(errorID), errors[0].ID) 53} 54 55func TestErrorsStackTrace(t *testing.T) { 56 modelError := sendError(t, &errorsStackTracer{ 57 "zing", newErrorsStackTrace(0, 2), 58 }) 59 exception := modelError.Exception 60 stacktrace := exception.Stacktrace 61 assert.Equal(t, "zing", exception.Message) 62 assert.Equal(t, "go.elastic.co/apm_test", exception.Module) 63 assert.Equal(t, "errorsStackTracer", exception.Type) 64 require.Len(t, stacktrace, 2) 65 assert.Equal(t, "newErrorsStackTrace", stacktrace[0].Function) 66 assert.Equal(t, "TestErrorsStackTrace", stacktrace[1].Function) 67} 68 69func TestErrorsStackTraceLimit(t *testing.T) { 70 defer os.Unsetenv("ELASTIC_APM_STACK_TRACE_LIMIT") 71 const n = 2 72 for i := -1; i < n; i++ { 73 os.Setenv("ELASTIC_APM_STACK_TRACE_LIMIT", strconv.Itoa(i)) 74 modelError := sendError(t, &errorsStackTracer{ 75 "zing", newErrorsStackTrace(0, n), 76 }) 77 stacktrace := modelError.Exception.Stacktrace 78 if i == -1 { 79 require.Len(t, stacktrace, n) 80 } else { 81 require.Len(t, stacktrace, i) 82 } 83 } 84} 85 86func TestInternalStackTrace(t *testing.T) { 87 // Absolute path on both windows (UNC) and *nix 88 abspath := filepath.FromSlash("//abs/path/file.go") 89 modelError := sendError(t, &internalStackTracer{ 90 "zing", []stacktrace.Frame{ 91 {Function: "pkg/path.FuncName"}, 92 {Function: "FuncName2", File: abspath, Line: 123}, 93 {Function: "encoding/json.Marshal"}, 94 }, 95 }) 96 exception := modelError.Exception 97 stacktrace := exception.Stacktrace 98 assert.Equal(t, "zing", exception.Message) 99 assert.Equal(t, "go.elastic.co/apm_test", exception.Module) 100 assert.Equal(t, "internalStackTracer", exception.Type) 101 assert.Equal(t, []model.StacktraceFrame{{ 102 Function: "FuncName", 103 Module: "pkg/path", 104 }, { 105 AbsolutePath: abspath, 106 Function: "FuncName2", 107 File: "file.go", 108 Line: 123, 109 }, { 110 Function: "Marshal", 111 Module: "encoding/json", 112 LibraryFrame: true, 113 }}, stacktrace) 114} 115 116func TestInternalStackTraceLimit(t *testing.T) { 117 inFrames := []stacktrace.Frame{ 118 {Function: "pkg/path.FuncName"}, 119 {Function: "FuncName2", Line: 123}, 120 {Function: "encoding/json.Marshal"}, 121 } 122 outFrames := []model.StacktraceFrame{{ 123 Function: "FuncName", 124 Module: "pkg/path", 125 }, { 126 Function: "FuncName2", 127 Line: 123, 128 }, { 129 Function: "Marshal", 130 Module: "encoding/json", 131 LibraryFrame: true, 132 }} 133 134 defer os.Unsetenv("ELASTIC_APM_STACK_TRACE_LIMIT") 135 for i := -1; i < len(inFrames); i++ { 136 os.Setenv("ELASTIC_APM_STACK_TRACE_LIMIT", strconv.Itoa(i)) 137 modelError := sendError(t, &internalStackTracer{ 138 "zing", []stacktrace.Frame{ 139 {Function: "pkg/path.FuncName"}, 140 {Function: "FuncName2", Line: 123}, 141 {Function: "encoding/json.Marshal"}, 142 }, 143 }) 144 stacktrace := modelError.Exception.Stacktrace 145 if i == 0 { 146 assert.Nil(t, stacktrace) 147 continue 148 } 149 expect := outFrames 150 if i > 0 { 151 expect = expect[:i] 152 } 153 assert.Equal(t, expect, stacktrace) 154 } 155} 156 157func TestErrorAutoStackTraceReuse(t *testing.T) { 158 tracer, r := transporttest.NewRecorderTracer() 159 defer tracer.Close() 160 161 err := fmt.Errorf("hullo") // no stacktrace attached 162 for i := 0; i < 1000; i++ { 163 tracer.NewError(err).Send() 164 } 165 tracer.Flush(nil) 166 167 // The previously sent error objects should have 168 // been reset and will be reused. We reuse the 169 // stacktrace slice. See elastic/apm-agent-go#204. 170 for i := 0; i < 1000; i++ { 171 tracer.NewError(err).Send() 172 } 173 tracer.Flush(nil) 174 175 payloads := r.Payloads() 176 assert.NotEmpty(t, payloads.Errors) 177 for _, e := range payloads.Errors { 178 assert.NotEqual(t, "", e.Culprit) 179 assert.NotEmpty(t, e.Exception.Stacktrace) 180 } 181} 182 183func TestCaptureErrorNoTransaction(t *testing.T) { 184 // When there's no transaction or span in the context, 185 // CaptureError returns Error with nil ErrorData as it has no tracer with 186 // which it can create the error. 187 e := apm.CaptureError(context.Background(), errors.New("boom")) 188 assert.Nil(t, e.ErrorData) 189 190 // Send is a no-op on a Error with nil ErrorData. 191 e.Send() 192} 193 194func TestErrorLogRecord(t *testing.T) { 195 tracer, recorder := transporttest.NewRecorderTracer() 196 defer tracer.Close() 197 198 error_ := tracer.NewErrorLog(apm.ErrorLogRecord{ 199 Message: "log-message", 200 Error: makeError("error-message"), 201 }) 202 error_.SetStacktrace(1) 203 error_.Send() 204 tracer.Flush(nil) 205 206 payloads := recorder.Payloads() 207 require.Len(t, payloads.Errors, 1) 208 err0 := payloads.Errors[0] 209 assert.Equal(t, "log-message", err0.Log.Message) 210 assert.Equal(t, "error-message", err0.Exception.Message) 211 require.NotEmpty(t, err0.Log.Stacktrace) 212 require.NotEmpty(t, err0.Exception.Stacktrace) 213 assert.Equal(t, err0.Log.Stacktrace[0].Function, "TestErrorLogRecord") 214 assert.Equal(t, err0.Exception.Stacktrace[0].Function, "makeError") 215 assert.Equal(t, "makeError", err0.Culprit) // based on exception stacktrace 216} 217 218func TestErrorCauserInterface(t *testing.T) { 219 type Causer interface { 220 Cause() error 221 } 222 var e Causer = apm.CaptureError(context.Background(), errors.New("boom")) 223 assert.EqualError(t, e.Cause(), "boom") 224} 225 226func TestErrorNilCauser(t *testing.T) { 227 var e *apm.Error 228 assert.Nil(t, e.Cause()) 229 230 e = &apm.Error{} 231 assert.Nil(t, e.Cause()) 232} 233 234func TestErrorErrorInterface(t *testing.T) { 235 var e error = apm.CaptureError(context.Background(), errors.New("boom")) 236 assert.EqualError(t, e, "boom") 237} 238 239func TestErrorNilError(t *testing.T) { 240 var e *apm.Error 241 assert.EqualError(t, e, "[EMPTY]") 242 243 e = &apm.Error{} 244 assert.EqualError(t, e, "") 245} 246 247func TestErrorTransactionSampled(t *testing.T) { 248 _, _, errors := apmtest.WithTransaction(func(ctx context.Context) { 249 apm.TransactionFromContext(ctx).Type = "foo" 250 apm.CaptureError(ctx, errors.New("boom")).Send() 251 252 span, ctx := apm.StartSpan(ctx, "name", "type") 253 defer span.End() 254 apm.CaptureError(ctx, errors.New("boom")).Send() 255 }) 256 assertErrorTransactionSampled(t, errors[0], true) 257 assertErrorTransactionSampled(t, errors[1], true) 258 assert.Equal(t, "foo", errors[0].Transaction.Type) 259 assert.Equal(t, "foo", errors[1].Transaction.Type) 260} 261 262func TestErrorTransactionNotSampled(t *testing.T) { 263 tracer, recorder := transporttest.NewRecorderTracer() 264 defer tracer.Close() 265 tracer.SetSampler(apm.NewRatioSampler(0)) 266 267 tx := tracer.StartTransaction("name", "type") 268 ctx := apm.ContextWithTransaction(context.Background(), tx) 269 apm.CaptureError(ctx, errors.New("boom")).Send() 270 271 tracer.Flush(nil) 272 payloads := recorder.Payloads() 273 require.Len(t, payloads.Errors, 1) 274 assertErrorTransactionSampled(t, payloads.Errors[0], false) 275} 276 277func TestErrorTransactionSampledNoTransaction(t *testing.T) { 278 tracer, recorder := transporttest.NewRecorderTracer() 279 defer tracer.Close() 280 281 tracer.NewError(errors.New("boom")).Send() 282 tracer.Flush(nil) 283 payloads := recorder.Payloads() 284 require.Len(t, payloads.Errors, 1) 285 assert.Nil(t, payloads.Errors[0].Transaction.Sampled) 286} 287 288func TestErrorTransactionCustomContext(t *testing.T) { 289 tracer, recorder := transporttest.NewRecorderTracer() 290 defer tracer.Close() 291 292 tx := tracer.StartTransaction("name", "type") 293 tx.Context.SetCustom("k1", "v1") 294 tx.Context.SetCustom("k2", "v2") 295 ctx := apm.ContextWithTransaction(context.Background(), tx) 296 apm.CaptureError(ctx, errors.New("boom")).Send() 297 298 _, ctx = apm.StartSpan(ctx, "foo", "bar") 299 apm.CaptureError(ctx, errors.New("boom")).Send() 300 301 // Create an error with custom context set before setting 302 // the transaction. Such custom context should override 303 // whatever is carried over from the transaction. 304 e := tracer.NewError(errors.New("boom")) 305 e.Context.SetCustom("k1", "!!") 306 e.Context.SetCustom("k3", "v3") 307 e.SetTransaction(tx) 308 e.Send() 309 310 tracer.Flush(nil) 311 payloads := recorder.Payloads() 312 require.Len(t, payloads.Errors, 3) 313 314 assert.Equal(t, model.IfaceMap{ 315 {Key: "k1", Value: "v1"}, 316 {Key: "k2", Value: "v2"}, 317 }, payloads.Errors[0].Context.Custom) 318 319 assert.Equal(t, model.IfaceMap{ 320 {Key: "k1", Value: "v1"}, 321 {Key: "k2", Value: "v2"}, 322 }, payloads.Errors[1].Context.Custom) 323 324 assert.Equal(t, model.IfaceMap{ 325 {Key: "k1", Value: "!!"}, 326 {Key: "k2", Value: "v2"}, 327 {Key: "k3", Value: "v3"}, 328 }, payloads.Errors[2].Context.Custom) 329} 330 331func TestErrorDetailer(t *testing.T) { 332 type error1 struct{ error } 333 apm.RegisterTypeErrorDetailer(reflect.TypeOf(error1{}), apm.ErrorDetailerFunc(func(err error, details *apm.ErrorDetails) { 334 details.SetAttr("a", "error1") 335 })) 336 337 type error2 struct{ error } 338 apm.RegisterTypeErrorDetailer(reflect.TypeOf(&error2{}), apm.ErrorDetailerFunc(func(err error, details *apm.ErrorDetails) { 339 details.SetAttr("b", "*error2") 340 })) 341 342 apm.RegisterErrorDetailer(apm.ErrorDetailerFunc(func(err error, details *apm.ErrorDetails) { 343 // NOTE(axw) ErrorDetailers can't be _unregistered_, 344 // so we check the error type so as not to interfere 345 // with other tests. 346 switch err.(type) { 347 case error1, *error2: 348 details.SetAttr("c", "both") 349 } 350 })) 351 352 _, _, errs := apmtest.WithTransaction(func(ctx context.Context) { 353 apm.CaptureError(ctx, error1{errors.New("error1")}).Send() 354 apm.CaptureError(ctx, &error2{errors.New("error2")}).Send() 355 }) 356 require.Len(t, errs, 2) 357 assert.Equal(t, map[string]interface{}{"a": "error1", "c": "both"}, errs[0].Exception.Attributes) 358 assert.Equal(t, map[string]interface{}{"b": "*error2", "c": "both"}, errs[1].Exception.Attributes) 359} 360 361func TestStdlibErrorDetailers(t *testing.T) { 362 t.Run("syscall.Errno", func(t *testing.T) { 363 _, _, errs := apmtest.WithTransaction(func(ctx context.Context) { 364 apm.CaptureError(ctx, syscall.Errno(syscall.EAGAIN)).Send() 365 }) 366 require.Len(t, errs, 1) 367 368 if runtime.GOOS == "windows" { 369 // There's currently no equivalent of unix.ErrnoName for Windows. 370 assert.Equal(t, model.ExceptionCode{Number: float64(syscall.EAGAIN)}, errs[0].Exception.Code) 371 } else { 372 assert.Equal(t, model.ExceptionCode{String: "EAGAIN"}, errs[0].Exception.Code) 373 } 374 375 assert.Equal(t, map[string]interface{}{ 376 "temporary": true, 377 "timeout": true, 378 }, errs[0].Exception.Attributes) 379 }) 380 381 test := func(err error, expectedAttrs map[string]interface{}) { 382 t.Run(fmt.Sprintf("%T", err), func(t *testing.T) { 383 _, _, errs := apmtest.WithTransaction(func(ctx context.Context) { 384 apm.CaptureError(ctx, err).Send() 385 }) 386 require.Len(t, errs, 1) 387 assert.Equal(t, expectedAttrs, errs[0].Exception.Attributes) 388 }) 389 } 390 type attrmap map[string]interface{} 391 392 test(&net.OpError{ 393 Err: errors.New("cause"), 394 Op: "read", 395 Net: "tcp", 396 Source: &net.TCPAddr{ 397 IP: net.IPv6loopback, 398 Port: 1234, 399 }, 400 }, attrmap{"op": "read", "net": "tcp", "source": "tcp:[::1]:1234"}) 401 402 test(&os.LinkError{ 403 Err: errors.New("cause"), 404 Op: "symlink", 405 Old: "/old", 406 New: "/new", 407 }, attrmap{"op": "symlink", "old": "/old", "new": "/new"}) 408 409 test(&os.PathError{ 410 Err: errors.New("cause"), 411 Op: "open", 412 Path: "/dev/null", 413 }, attrmap{"op": "open", "path": "/dev/null"}) 414 415 test(&os.SyscallError{ 416 Err: errors.New("cause"), 417 Syscall: "connect", 418 }, attrmap{"syscall": "connect"}) 419} 420 421func assertErrorTransactionSampled(t *testing.T, e model.Error, sampled bool) { 422 assert.Equal(t, &sampled, e.Transaction.Sampled) 423 if sampled { 424 assert.NotEmpty(t, e.Transaction.Type) 425 } else { 426 assert.Empty(t, e.Transaction.Type) 427 } 428} 429 430func makeError(msg string) error { 431 return errors.New(msg) 432} 433 434func sendError(t *testing.T, err error, f ...func(*apm.Error)) model.Error { 435 tracer, r := transporttest.NewRecorderTracer() 436 defer tracer.Close() 437 438 error_ := tracer.NewError(err) 439 for _, f := range f { 440 f(error_) 441 } 442 443 error_.Send() 444 tracer.Flush(nil) 445 446 payloads := r.Payloads() 447 return payloads.Errors[0] 448} 449 450type errorsStackTracer struct { 451 message string 452 stackTrace errors.StackTrace 453} 454 455func (e *errorsStackTracer) Error() string { 456 return e.message 457} 458 459func (e *errorsStackTracer) StackTrace() errors.StackTrace { 460 return e.stackTrace 461} 462 463func newErrorsStackTrace(skip, n int) errors.StackTrace { 464 callers := make([]uintptr, 2) 465 callers = callers[:runtime.Callers(1, callers)] 466 467 var ( 468 uintptrType = reflect.TypeOf(uintptr(0)) 469 errorsFrameType = reflect.TypeOf(*new(errors.Frame)) 470 runtimeFrameType = reflect.TypeOf(runtime.Frame{}) 471 ) 472 473 var frames []errors.Frame 474 switch { 475 case errorsFrameType.ConvertibleTo(uintptrType): 476 frames = make([]errors.Frame, len(callers)) 477 for i, pc := range callers { 478 reflect.ValueOf(&frames[i]).Elem().Set(reflect.ValueOf(pc).Convert(errorsFrameType)) 479 } 480 case errorsFrameType.ConvertibleTo(runtimeFrameType): 481 fs := runtime.CallersFrames(callers) 482 for { 483 var frame errors.Frame 484 runtimeFrame, more := fs.Next() 485 reflect.ValueOf(&frame).Elem().Set(reflect.ValueOf(runtimeFrame).Convert(errorsFrameType)) 486 frames = append(frames, frame) 487 if !more { 488 break 489 } 490 } 491 default: 492 panic(fmt.Errorf("unhandled errors.Frame type %s", errorsFrameType)) 493 } 494 return errors.StackTrace(frames) 495} 496 497type internalStackTracer struct { 498 message string 499 frames []stacktrace.Frame 500} 501 502func (e *internalStackTracer) Error() string { 503 return e.message 504} 505 506func (e *internalStackTracer) StackTrace() []stacktrace.Frame { 507 return e.frames 508} 509