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