From 0d3bd975a6cae3a93eb28848e459c987b0643b0e Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Fri, 2 Dec 2022 16:20:37 -0500 Subject: [PATCH 01/10] 99% test coverage for encode.go --- v3/internal/jsonx/encode_test.go | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/v3/internal/jsonx/encode_test.go b/v3/internal/jsonx/encode_test.go index cc0f5934c..ef5ef38b0 100644 --- a/v3/internal/jsonx/encode_test.go +++ b/v3/internal/jsonx/encode_test.go @@ -5,6 +5,7 @@ package jsonx import ( "bytes" + "fmt" "math" "testing" ) @@ -28,6 +29,30 @@ func TestAppendFloat(t *testing.T) { } } +func TestAppendFloat32(t *testing.T) { + buf := &bytes.Buffer{} + + err := AppendFloat32(buf, float32(math.NaN())) + if err == nil { + t.Error("AppendFloat(NaN) should return an error") + } + + err = AppendFloat32(buf, float32(math.Inf(1))) + if err == nil { + t.Error("AppendFloat(+Inf) should return an error") + } + + err = AppendFloat32(buf, float32(math.Inf(-1))) + if err == nil { + t.Error("AppendFloat(-Inf) should return an error") + } + + err = AppendFloat32(buf, float32(12.5)) + if err != nil { + t.Error("AppendFloat(12.5) should not return an error") + } +} + func TestAppendFloats(t *testing.T) { buf := &bytes.Buffer{} @@ -166,6 +191,15 @@ var encodeStringTests = []struct { {"\\", `"\\"`}, {`"`, `"\""`}, {"the\u2028quick\t\nbrown\u2029fox", `"the\u2028quick\t\nbrown\u2029fox"`}, + + //extra edge cases + {string([]byte{237, 159, 193}), `"\ufffd\ufffd\ufffd"`}, // invalid utf8 + {string([]byte{55, 237, 159, 193, 55}), `"7\ufffd\ufffd\ufffd7"`}, // invalid utf8 surrounded by valid utf8 + {`abcdefghijklmnopqrstuvwxyz1234567890`, `"abcdefghijklmnopqrstuvwxyz1234567890"`}, // alphanumeric + {"'", `"'"`}, + {``, `""`}, + {`\`, `"\\"`}, + {fmt.Sprintf("%c", rune(65533)), fmt.Sprintf("\"%c\"", rune(65533))}, // invalid rune utf8 symbol (valid utf8) } func TestAppendString(t *testing.T) { @@ -181,6 +215,41 @@ func TestAppendString(t *testing.T) { } } +func TestAppendStringArray(t *testing.T) { + buf := &bytes.Buffer{} + + var encodeStringArrayTests = []struct { + in []string + out string + }{ + { + in: []string{ + "hi", + "foo", + }, + out: `["hi","foo"]`, + }, + { + in: []string{ + "foo", + }, + out: `["foo"]`, + }, + { + in: []string{}, + out: `[]`, + }, + } + + for _, tt := range encodeStringArrayTests { + buf.Reset() + + AppendStringArray(buf, tt.in...) + if got := buf.String(); got != tt.out { + t.Errorf("AppendString(%q) = %#q, want %#q", tt.in, got, tt.out) + } + } +} func BenchmarkAppendString(b *testing.B) { buf := &bytes.Buffer{} From 09541bf52f2730eec0b0c1e298720aa9a639a6aa Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 5 Dec 2022 11:00:49 -0500 Subject: [PATCH 02/10] guard payload harvest from panic --- v3/newrelic/attributes_from_internal.go | 10 ++++------ v3/newrelic/internal_app.go | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/v3/newrelic/attributes_from_internal.go b/v3/newrelic/attributes_from_internal.go index 96d1e829c..28e8b0c37 100644 --- a/v3/newrelic/attributes_from_internal.go +++ b/v3/newrelic/attributes_from_internal.go @@ -275,10 +275,8 @@ func (attr agentAttributes) Add(id string, stringVal string, otherVal interface{ } } -// // Remove is used to remove agent attributes. // It is not an error if the attribute wasn't present to begin with. -// func (attr agentAttributes) Remove(id string) { if _, ok := attr[id]; ok { delete(attr, id) @@ -453,14 +451,14 @@ func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) { } func agentAttributesJSON(a *attributes, buf *bytes.Buffer, d destinationSet) { - if nil == a { + if a == nil { buf.WriteString("{}") return } w := jsonFieldsWriter{buf: buf} buf.WriteByte('{') for id, val := range a.Agent { - if 0 != a.config.agentDests[id]&d { + if a.config.agentDests[id]&d != 0 { if val.stringVal != "" { w.stringField(id, val.stringVal) } else { @@ -478,12 +476,12 @@ func userAttributesJSON(a *attributes, buf *bytes.Buffer, d destinationSet, extr w := jsonFieldsWriter{buf: buf} for key, val := range extraAttributes { outputDest := applyAttributeConfig(a.config, key, d) - if 0 != outputDest&d { + if outputDest&d != 0 { writeAttributeValueJSON(&w, key, val) } } for name, atr := range a.user { - if 0 != atr.dests&d { + if atr.dests&d != 0 { if _, found := extraAttributes[name]; found { continue } diff --git a/v3/newrelic/internal_app.go b/v3/newrelic/internal_app.go index 71042f2c5..1d8b9d474 100644 --- a/v3/newrelic/internal_app.go +++ b/v3/newrelic/internal_app.go @@ -71,16 +71,30 @@ func (app *app) doHarvest(h *harvest, harvestStart time.Time, run *appRun) { payloads := h.Payloads(app.config.DistributedTracer.Enabled) for _, p := range payloads { cmd := p.EndpointMethod() + var data []byte + + defer func() { + if r := recover(); r != nil { + app.Warn("panic occured when creating harvest data", map[string]interface{}{ + "cmd": cmd, + "panic": r, + }) + + // make sure the loop continues + data = nil + } + }() + data, err := p.Data(run.Reply.RunID.String(), harvestStart) - if nil != err { + if err != nil { app.Warn("unable to create harvest data", map[string]interface{}{ "cmd": cmd, "error": err.Error(), }) continue } - if nil == data { + if data == nil { continue } @@ -103,7 +117,7 @@ func (app *app) doHarvest(h *harvest, harvestStart time.Time, run *appRun) { return } - if nil != resp.Err { + if resp.Err != nil { app.Warn("harvest failure", map[string]interface{}{ "cmd": cmd, "error": resp.Err.Error(), From e6ffdc6826e1c02f2c4194b546c7c9160381365e Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Wed, 7 Dec 2022 12:36:17 -0500 Subject: [PATCH 03/10] Notice Expected Errors A new API has been added that will allow users to notice errors without triggering their error alerts or metrics: NoticeExpectedError() ` --- v3/examples/server/main.go | 8 ++ v3/newrelic/errors_from_internal.go | 5 +- v3/newrelic/harvest.go | 6 +- v3/newrelic/harvest_test.go | 30 +++++++ .../internal_errors_stacktrace_test.go | 2 +- v3/newrelic/internal_errors_test.go | 2 +- v3/newrelic/internal_txn.go | 29 ++++--- v3/newrelic/intrinsics.go | 10 ++- v3/newrelic/metric_names.go | 4 +- v3/newrelic/tracing.go | 48 +++++++---- v3/newrelic/transaction.go | 86 +++++++++++++------ v3/newrelic/txn_trace.go | 2 +- 12 files changed, 171 insertions(+), 61 deletions(-) diff --git a/v3/examples/server/main.go b/v3/examples/server/main.go index 9919539c1..c012e349b 100644 --- a/v3/examples/server/main.go +++ b/v3/examples/server/main.go @@ -31,6 +31,13 @@ func noticeError(w http.ResponseWriter, r *http.Request) { txn.NoticeError(errors.New("my error message")) } +func noticeExpectedError(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "noticing an error") + + txn := newrelic.FromContext(r.Context()) + txn.NoticeExpectedError(errors.New("my expected error message")) +} + func noticeErrorWithAttributes(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "noticing an error") @@ -273,6 +280,7 @@ func main() { http.HandleFunc(newrelic.WrapHandleFunc(app, "/", index)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/version", versionHandler)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/notice_error", noticeError)) + http.HandleFunc(newrelic.WrapHandleFunc(app, "/notice_expected_error", noticeExpectedError)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/notice_error_with_attributes", noticeErrorWithAttributes)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/custom_event", customEvent)) http.HandleFunc(newrelic.WrapHandleFunc(app, "/set_name", setName)) diff --git a/v3/newrelic/errors_from_internal.go b/v3/newrelic/errors_from_internal.go index 7813d102f..9b174f3f0 100644 --- a/v3/newrelic/errors_from_internal.go +++ b/v3/newrelic/errors_from_internal.go @@ -61,6 +61,7 @@ type errorData struct { Msg string Klass string SpanID string + Expect bool } // txnError combines error data with information about a transaction. txnError is used for @@ -113,7 +114,7 @@ func (h *tracedError) WriteJSON(buf *bytes.Buffer) { buf.WriteByte(',') buf.WriteString(`"intrinsics"`) buf.WriteByte(':') - intrinsicsJSON(&h.txnEvent, buf) + intrinsicsJSON(&h.txnEvent, buf, h.errorData.Expect) if nil != h.Stack { buf.WriteByte(',') buf.WriteString(`"stack_trace"`) @@ -152,7 +153,7 @@ func mergeTxnErrors(errors *harvestErrors, errs txnErrors, txnEvent txnEvent) { } func (errors harvestErrors) Data(agentRunID string, harvestStart time.Time) ([]byte, error) { - if 0 == len(errors) { + if len(errors) == 0 { return nil, nil } estimate := 1024 * len(errors) diff --git a/v3/newrelic/harvest.go b/v3/newrelic/harvest.go index 3a7e1cf61..72159e49a 100644 --- a/v3/newrelic/harvest.go +++ b/v3/newrelic/harvest.go @@ -327,12 +327,16 @@ func createTxnMetrics(args *txnData, metrics *metricTable) { } // Error Metrics - if args.HasErrors() { + if args.NoticeErrors() { metrics.addSingleCount(errorsRollupMetric.all, forced) metrics.addSingleCount(errorsRollupMetric.webOrOther(args.IsWeb), forced) metrics.addSingleCount(errorsPrefix+args.FinalName, forced) } + if args.HasExpectedErrors() { + metrics.addSingleCount(expectedErrorsRollupMetric.all, forced) + } + // Queueing Metrics if args.Queuing > 0 { metrics.addDuration(queueMetric, "", args.Queuing, args.Queuing, forced) diff --git a/v3/newrelic/harvest_test.go b/v3/newrelic/harvest_test.go index 0acbe6ae1..b175e2232 100644 --- a/v3/newrelic/harvest_test.go +++ b/v3/newrelic/harvest_test.go @@ -771,6 +771,7 @@ func TestCreateTxnMetrics(t *testing.T) { webName := "WebTransaction/zip/zap" backgroundName := "OtherTransaction/zip/zap" args := &txnData{} + args.noticeErrors = true args.Duration = 123 * time.Second args.TotalTime = 150 * time.Second args.ApdexThreshold = 2 * time.Second @@ -803,6 +804,7 @@ func TestCreateTxnMetrics(t *testing.T) { args.FinalName = webName args.IsWeb = true args.Errors = nil + args.noticeErrors = false args.Zone = apdexTolerating metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) @@ -821,6 +823,7 @@ func TestCreateTxnMetrics(t *testing.T) { args.FinalName = backgroundName args.IsWeb = false args.Errors = txnErrors + args.noticeErrors = true args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) @@ -838,9 +841,32 @@ func TestCreateTxnMetrics(t *testing.T) { {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, }) + // Verify expected errors metrics + args.FinalName = backgroundName + args.IsWeb = false + args.Errors = txnErrors + args.noticeErrors = false + args.expectedErrors = true + args.Zone = apdexNone + metrics = newMetricTable(100, time.Now()) + createTxnMetrics(args, metrics) + expectMetrics(t, metrics, []internal.WantMetric{ + {Name: backgroundName, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, + {Name: backgroundRollup, Scope: "", Forced: true, Data: []float64{1, 123, 0, 123, 123, 123 * 123}}, + {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, + {Name: "OtherTransactionTotalTime/zip/zap", Scope: "", Forced: false, Data: []float64{1, 150, 150, 150, 150, 150 * 150}}, + {Name: "ErrorsExpected/all", Scope: "", Forced: true, Data: []float64{1, 0, 0, 0, 0, 0}}, + {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, + {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: []float64{1, 123, 123, 123, 123, 123 * 123}}, + {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, + {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allOther", Scope: "", Forced: false, Data: []float64{1, 0, 0, 0, 0, 0}}, + }) + args.FinalName = backgroundName args.IsWeb = false args.Errors = nil + args.noticeErrors = false + args.expectedErrors = false args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) @@ -889,6 +915,7 @@ func TestCreateTxnMetricsOldCAT(t *testing.T) { args.FinalName = webName args.IsWeb = true args.Errors = txnErrors + args.noticeErrors = true args.Zone = apdexTolerating metrics := newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) @@ -908,6 +935,7 @@ func TestCreateTxnMetricsOldCAT(t *testing.T) { args.FinalName = webName args.IsWeb = true args.Errors = nil + args.noticeErrors = false args.Zone = apdexTolerating metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) @@ -924,6 +952,7 @@ func TestCreateTxnMetricsOldCAT(t *testing.T) { args.FinalName = backgroundName args.IsWeb = false args.Errors = txnErrors + args.noticeErrors = true args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) @@ -940,6 +969,7 @@ func TestCreateTxnMetricsOldCAT(t *testing.T) { args.FinalName = backgroundName args.IsWeb = false args.Errors = nil + args.noticeErrors = false args.Zone = apdexNone metrics = newMetricTable(100, time.Now()) createTxnMetrics(args, metrics) diff --git a/v3/newrelic/internal_errors_stacktrace_test.go b/v3/newrelic/internal_errors_stacktrace_test.go index 6ff3a9304..a7fae6fa6 100644 --- a/v3/newrelic/internal_errors_stacktrace_test.go +++ b/v3/newrelic/internal_errors_stacktrace_test.go @@ -64,7 +64,7 @@ func TestStackTrace(t *testing.T) { } for idx, tc := range testcases { - data, err := errDataFromError(tc.Error) + data, err := errDataFromError(tc.Error, false) if err != nil { t.Errorf("testcase %d: got error: %v", idx, err) continue diff --git a/v3/newrelic/internal_errors_test.go b/v3/newrelic/internal_errors_test.go index d5fa710cd..aa618d155 100644 --- a/v3/newrelic/internal_errors_test.go +++ b/v3/newrelic/internal_errors_test.go @@ -636,7 +636,7 @@ func TestErrorClass(t *testing.T) { } for idx, tc := range testcases { - data, err := errDataFromError(tc.Error) + data, err := errDataFromError(tc.Error, false) if err != nil { t.Errorf("testcase %d: got error: %v", idx, err) continue diff --git a/v3/newrelic/internal_txn.go b/v3/newrelic/internal_txn.go index ee89456f6..b98fb45c2 100644 --- a/v3/newrelic/internal_txn.go +++ b/v3/newrelic/internal_txn.go @@ -366,7 +366,7 @@ func headersJustWritten(thd *thread, code int, hdr http.Header) { if txn.appRun.responseCodeIsError(code) { e := txnErrorFromResponseCode(time.Now(), code) e.Stack = getStackTrace() - thd.noticeErrorInternal(e) + thd.noticeErrorInternal(e, false) } } @@ -425,7 +425,7 @@ func (thd *thread) End(recovered interface{}) error { if nil != recovered { e := txnErrorFromPanic(time.Now(), recovered) e.Stack = getStackTrace() - thd.noticeErrorInternal(e) + thd.noticeErrorInternal(e, false) log.Println(string(debug.Stack())) } @@ -447,7 +447,7 @@ func (thd *thread) End(recovered interface{}) error { txn.ApdexThreshold = internal.CalculateApdexThreshold(txn.Reply, txn.FinalName) if txn.getsApdex() { - if txn.HasErrors() { + if txn.HasErrors() && txn.NoticeErrors() { txn.Zone = apdexFailing } else { txn.Zone = calculateApdexZone(txn.ApdexThreshold, txn.Duration) @@ -461,7 +461,7 @@ func (thd *thread) End(recovered interface{}) error { "name": txn.FinalName, "duration_ms": txn.Duration.Seconds() * 1000.0, "ignored": txn.ignore, - "app_connected": "" != txn.Reply.RunID, + "app_connected": txn.Reply.RunID != "", }) } @@ -559,12 +559,18 @@ const ( securityPolicyErrorMsg = "message removed by security policy" ) -func (thd *thread) noticeErrorInternal(err errorData) error { +func (thd *thread) noticeErrorInternal(err errorData, expect bool) error { txn := thd.txn if !txn.Config.ErrorCollector.Enabled { return errorsDisabled } + if !expect { + thd.noticeErrors = true + } else { + thd.expectedErrors = true + } + if nil == txn.Errors { txn.Errors = newTxnErrors(maxTxnErrors) } @@ -643,12 +649,13 @@ func errorAttributesMethod(err error) map[string]interface{} { return nil } -func errDataFromError(input error) (data errorData, err error) { +func errDataFromError(input error, expect bool) (data errorData, err error) { cause := errorCause(input) data = errorData{ - When: time.Now(), - Msg: input.Error(), + When: time.Now(), + Msg: input.Error(), + Expect: expect, } if c := errorClassMethod(input); "" != c { @@ -700,7 +707,7 @@ func errDataFromError(input error) (data errorData, err error) { return data, nil } -func (thd *thread) NoticeError(input error) error { +func (thd *thread) NoticeError(input error, expect bool) error { txn := thd.txn txn.Lock() defer txn.Unlock() @@ -713,7 +720,7 @@ func (thd *thread) NoticeError(input error) error { return errNilError } - data, err := errDataFromError(input) + data, err := errDataFromError(input, expect) if nil != err { return err } @@ -722,7 +729,7 @@ func (thd *thread) NoticeError(input error) error { data.ExtraAttributes = nil } - return thd.noticeErrorInternal(data) + return thd.noticeErrorInternal(data, expect) } func (txn *txn) SetName(name string) error { diff --git a/v3/newrelic/intrinsics.go b/v3/newrelic/intrinsics.go index f626c8f41..39eff9648 100644 --- a/v3/newrelic/intrinsics.go +++ b/v3/newrelic/intrinsics.go @@ -7,13 +7,17 @@ import ( "bytes" ) +const ( + expectErrorAttr = "error.expected" +) + func addOptionalStringField(w *jsonFieldsWriter, key, value string) { if value != "" { w.stringField(key, value) } } -func intrinsicsJSON(e *txnEvent, buf *bytes.Buffer) { +func intrinsicsJSON(e *txnEvent, buf *bytes.Buffer, expect bool) { w := jsonFieldsWriter{buf: buf} buf.WriteByte('{') @@ -27,6 +31,10 @@ func intrinsicsJSON(e *txnEvent, buf *bytes.Buffer) { w.boolField("sampled", e.BetterCAT.Sampled) } + if expect { + w.stringField(expectErrorAttr, "true") + } + if e.CrossProcess.Used() { addOptionalStringField(&w, "client_cross_process_id", e.CrossProcess.ClientID) addOptionalStringField(&w, "trip_id", e.CrossProcess.TripID) diff --git a/v3/newrelic/metric_names.go b/v3/newrelic/metric_names.go index 7bb053e23..cc0883416 100644 --- a/v3/newrelic/metric_names.go +++ b/v3/newrelic/metric_names.go @@ -187,8 +187,8 @@ func (r rollupMetric) webOrOther(isWeb bool) string { } var ( - errorsRollupMetric = newRollupMetric("Errors/") - + errorsRollupMetric = newRollupMetric("Errors/") + expectedErrorsRollupMetric = newRollupMetric("ErrorsExpected/") // source.datanerd.us/agents/agent-specs/blob/master/APIs/external_segment.md // source.datanerd.us/agents/agent-specs/blob/master/APIs/external_cat.md // source.datanerd.us/agents/agent-specs/blob/master/Cross-Application-Tracing-PORTED.md diff --git a/v3/newrelic/tracing.go b/v3/newrelic/tracing.go index c17f2a2ad..d6a966401 100644 --- a/v3/newrelic/tracing.go +++ b/v3/newrelic/tracing.go @@ -63,21 +63,35 @@ func (bc *betterCAT) SetTraceAndTxnIDs(traceID string) { // txnData contains the recorded data of a transaction. type txnData struct { - txnEvent - IsWeb bool - Name string // Work in progress name. - Errors txnErrors // Lazily initialized. - Stop time.Time - ApdexThreshold time.Duration + IsWeb bool + SlowQueriesEnabled bool + noticeErrors bool // If errors are not expected or ignored, then true + expectedErrors bool stamp segmentStamp threadIDCounter uint64 + Name string // Work in progress name. + rootSpanID string + + txnEvent + TxnTrace txnTrace + + Stop time.Time + ApdexThreshold time.Duration + SlowQueryThreshold time.Duration + + SlowQueries *slowQueries + + // These better CAT supportability fields are left outside of + // TxnEvent.BetterCAT to minimize the size of transaction event memory. + DistributedTracingSupport distributedTracingSupport + TraceIDGenerator *internal.TraceIDGenerator ShouldCollectSpanEvents func() bool ShouldCreateSpanGUID func() bool - rootSpanID string rootSpanErrData *errorData + Errors txnErrors // Lazily initialized. SpanEvents []*spanEvent logs logEventHeap @@ -85,16 +99,6 @@ type txnData struct { datastoreSegments map[datastoreMetricKey]*metricData externalSegments map[externalMetricKey]*metricData messageSegments map[internal.MessageMetricKey]*metricData - - TxnTrace txnTrace - - SlowQueriesEnabled bool - SlowQueryThreshold time.Duration - SlowQueries *slowQueries - - // These better CAT supportability fields are left outside of - // TxnEvent.BetterCAT to minimize the size of transaction event memory. - DistributedTracingSupport distributedTracingSupport } func (t *txnData) saveTraceSegment(end segmentEnd, name string, attrs spanAttributeMap, externalGUID string) { @@ -320,11 +324,21 @@ const ( datastoreOperationUnknown = "other" ) +// NoticeErrors indicates whether the errors collected count towards error/ metrics +func (t *txnData) NoticeErrors() bool { + return t.noticeErrors +} + // HasErrors indicates whether the transaction had errors. func (t *txnData) HasErrors() bool { return len(t.Errors) > 0 } +// HasExpectedErrors is a special case where the txn has errors but we dont increment error metrics +func (t *txnData) HasExpectedErrors() bool { + return t.expectedErrors +} + func (t *txnData) time(now time.Time) segmentTime { // Update the stamp before using it so that a 0 stamp can be special. t.stamp++ diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index 78b3bed60..31f22b39e 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -43,14 +43,12 @@ func (txn *Transaction) End() { txn.thread.logAPIError(txn.thread.End(r), "end transaction", nil) } -// // SetOption allows the setting of some transaction TraceOption parameters // after the transaction has already been started, such as specifying a new // source code location for code-level metrics. // // The set of options should be the complete set you wish to have in effect, // just as if you were calling StartTransaction now with the same set of options. -// func (txn *Transaction) SetOption(options ...TraceOption) { if txn == nil || txn.thread == nil || txn.thread.txn == nil { return @@ -94,14 +92,14 @@ func (txn *Transaction) SetName(name string) { // NoticeError examines whether the error implements the following optional // methods: // -// // StackTrace records a stack trace -// StackTrace() []uintptr +// // StackTrace records a stack trace +// StackTrace() []uintptr // -// // ErrorClass sets the error's class -// ErrorClass() string +// // ErrorClass sets the error's class +// ErrorClass() string // -// // ErrorAttributes sets the errors attributes -// ErrorAttributes() map[string]interface{} +// // ErrorAttributes sets the errors attributes +// ErrorAttributes() map[string]interface{} // // The newrelic.Error type, which implements these methods, is the recommended // way to directly control the recorded error's message, class, stacktrace, @@ -113,7 +111,44 @@ func (txn *Transaction) NoticeError(err error) { if nil == txn.thread { return } - txn.thread.logAPIError(txn.thread.NoticeError(err), "notice error", nil) + txn.thread.logAPIError(txn.thread.NoticeError(err, false), "notice error", nil) +} + +// NoticeExpectedError records an error that was expected to occur. Errors recoreded with this +// method will not trigger any error alerts or count towards your error metrics. +// The Transaction saves the first five errors. +// For more control over the recorded error fields, see the +// newrelic.Error type. +// +// In certain situations, using this method may result in an error being +// recorded twice. Errors are automatically recorded when +// Transaction.WriteHeader receives a status code at or above 400 or strictly +// below 100 that is not in the IgnoreStatusCodes configuration list. This +// method is unaffected by the IgnoreStatusCodes configuration list. +// +// NoticeExpectedError examines whether the error implements the following optional +// methods: +// +// // StackTrace records a stack trace +// StackTrace() []uintptr +// +// // ErrorClass sets the error's class +// ErrorClass() string +// +// // ErrorAttributes sets the errors attributes +// ErrorAttributes() map[string]interface{} +// +// The newrelic.Error type, which implements these methods, is the recommended +// way to directly control the recorded error's message, class, stacktrace, +// and attributes. +func (txn *Transaction) NoticeExpectedError(err error) { + if nil == txn { + return + } + if nil == txn.thread { + return + } + txn.thread.logAPIError(txn.thread.NoticeError(err, true), "notice error", nil) } // AddAttribute adds a key value pair to the transaction event, errors, @@ -309,13 +344,11 @@ func (txn *Transaction) AcceptDistributedTraceHeaders(t TransportType, hdrs http txn.thread.logAPIError(txn.thread.AcceptDistributedTraceHeaders(t, hdrs), "accept trace payload", nil) } -// // AcceptDistributedTraceHeadersFromJSON works just like AcceptDistributedTraceHeaders(), except // that it takes the header data as a JSON string à la DistributedTraceHeadersFromJSON(). Additionally // (unlike AcceptDistributedTraceHeaders()) it returns an error if it was unable to successfully // convert the JSON string to http headers. There is no guarantee that the header data found in JSON // is correct beyond conforming to the expected types and syntax. -// func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, jsondata string) error { hdrs, err := DistributedTraceHeadersFromJSON(jsondata) if err != nil { @@ -325,7 +358,6 @@ func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, j return nil } -// // DistributedTraceHeadersFromJSON takes a set of distributed trace headers as a JSON-encoded string // and emits a http.Header value suitable for passing on to the // txn.AcceptDistributedTraceHeaders() function. @@ -336,27 +368,33 @@ func (txn *Transaction) AcceptDistributedTraceHeadersFromJSON(t TransportType, j // languages which may natively handle these header values as JSON strings. // // For example, given the input string -// `{"traceparent": "frob", "tracestate": "blorfl", "newrelic": "xyzzy"}` +// +// `{"traceparent": "frob", "tracestate": "blorfl", "newrelic": "xyzzy"}` +// // This will emit an http.Header value with headers "traceparent", "tracestate", and "newrelic". // Specifically: -// http.Header{ -// "Traceparent": {"frob"}, -// "Tracestate": {"blorfl"}, -// "Newrelic": {"xyzzy"}, -// } +// +// http.Header{ +// "Traceparent": {"frob"}, +// "Tracestate": {"blorfl"}, +// "Newrelic": {"xyzzy"}, +// } // // The JSON string must be a single object whose values may be strings or arrays of strings. // These are translated directly to http headers with singleton or multiple values. // In the case of multiple string values, these are translated to a multi-value HTTP // header. For example: -// `{"traceparent": "12345", "colors": ["red", "green", "blue"]}` +// +// `{"traceparent": "12345", "colors": ["red", "green", "blue"]}` +// // which produces -// http.Header{ -// "Traceparent": {"12345"}, -// "Colors": {"red", "green", "blue"}, -// } -// (Note that the HTTP headers are capitalized.) // +// http.Header{ +// "Traceparent": {"12345"}, +// "Colors": {"red", "green", "blue"}, +// } +// +// (Note that the HTTP headers are capitalized.) func DistributedTraceHeadersFromJSON(jsondata string) (hdrs http.Header, err error) { var raw interface{} hdrs = http.Header{} diff --git a/v3/newrelic/txn_trace.go b/v3/newrelic/txn_trace.go index 6050ba69e..2e925c5e4 100644 --- a/v3/newrelic/txn_trace.go +++ b/v3/newrelic/txn_trace.go @@ -281,7 +281,7 @@ func (trace *harvestTrace) writeJSON(buf *bytes.Buffer) { userAttributesJSON(trace.Attrs, buf, destTxnTrace, nil) buf.WriteByte(',') buf.WriteString(`"intrinsics":`) - intrinsicsJSON(&trace.txnEvent, buf) + intrinsicsJSON(&trace.txnEvent, buf, false) buf.WriteByte('}') // If the trace string pool is used, end another array here. From a346ec5b346e8a605608b9b8784d32223d0431a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergev=20=E2=82=B1?= <118327710+iot-defcon@users.noreply.github.com> Date: Sat, 10 Dec 2022 23:35:28 +0700 Subject: [PATCH 04/10] Update go.mod --- v3/integrations/nrecho-v4/go.mod | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/v3/integrations/nrecho-v4/go.mod b/v3/integrations/nrecho-v4/go.mod index 42b87228b..f51557781 100644 --- a/v3/integrations/nrecho-v4/go.mod +++ b/v3/integrations/nrecho-v4/go.mod @@ -5,6 +5,22 @@ module github.com/newrelic/go-agent/v3/integrations/nrecho-v4 go 1.17 require ( - github.com/labstack/echo/v4 v4.5.0 + github.com/labstack/echo/v4 v4.9.0 github.com/newrelic/go-agent/v3 v3.18.2 ) + +require ( + github.com/golang/protobuf v1.4.3 // indirect + github.com/labstack/gommon v0.3.1 // indirect + github.com/mattn/go-colorable v0.1.11 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect + golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/grpc v1.39.0 // indirect + google.golang.org/protobuf v1.25.0 // indirect +) From 41f07dbd92f719bb4a946222de9cc9abc029deb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergev=20=E2=82=B1?= <118327710+iot-defcon@users.noreply.github.com> Date: Sat, 10 Dec 2022 23:41:59 +0700 Subject: [PATCH 05/10] Update go.mod --- v3/integrations/nrstan/test/go.mod | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/integrations/nrstan/test/go.mod b/v3/integrations/nrstan/test/go.mod index ccb0ba0a9..aaeff8656 100644 --- a/v3/integrations/nrstan/test/go.mod +++ b/v3/integrations/nrstan/test/go.mod @@ -6,9 +6,9 @@ module github.com/newrelic/go-agent/v3/integrations/nrstan/test go 1.13 require ( - github.com/nats-io/nats-streaming-server v0.24.1 - github.com/nats-io/stan.go v0.10.2 - github.com/newrelic/go-agent/v3 v3.4.0 + github.com/nats-io/nats-streaming-server v0.24.3 + github.com/nats-io/stan.go v0.10.3 + github.com/newrelic/go-agent/v3 v3.18.2 github.com/newrelic/go-agent/v3/integrations/nrstan v0.0.0 ) From ca3a0c19012d7bf08bb63b3db981d276441a33e5 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 12 Dec 2022 16:14:34 -0500 Subject: [PATCH 06/10] community plus header updated --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index d8880328f..08ef49578 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ - -[![Community Plus header](https://github.com/newrelic/opensource-website/raw/master/src/images/categories/Community_Plus.png)](https://opensource.newrelic.com/oss-category/#community-plus) +[![Community Plus header](https://github.com/newrelic/opensource-website/raw/main/src/images/categories/Community_Plus.png)](https://opensource.newrelic.com/oss-category/#community-plus) # New Relic Go Agent [![GoDoc](https://godoc.org/github.com/newrelic/go-agent?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/newrelic/) [![Go Report Card](https://goreportcard.com/badge/github.com/newrelic/go-agent)](https://goreportcard.com/report/github.com/newrelic/go-agent) From 39824ad18cc3c99c7f8a8bde378e5b9d0e0167c3 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Mon, 12 Dec 2022 16:22:51 -0500 Subject: [PATCH 07/10] pull requests must be made against develop --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e457032e7..766c8b2de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ Before submitting an Issue, please search for similar ones in the ## Pull Requests -Pull requests must pass all automated tests and must be reviewed by at least one maintaining engineer before being merged. +Pull requests must pass all automated tests and must be reviewed by at least one maintaining engineer before being merged. Please contribute all pull requests against the `develop` branch, which is where we stage changes ahead of a release and run our most complete suite of tests. When contributing a new integration package, please follow the [Writing a New Integration Package](https://github.com/newrelic/go-agent/wiki/Writing-a-New-Integration-Package) wiki page. From 09a58e214c0b1f14e2d66679c570491e1a48b75d Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Tue, 13 Dec 2022 15:10:58 -0500 Subject: [PATCH 08/10] 3.20.2 changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9670337b2..7e7068b87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 3.20.2 + +### Added +* New `NoticeExpectedError()` method allows you to capture errors that you are expecting to handle, without triggering alerts + +### Fixed +* More defensive harvest cycle code that will avoid crashing even in the event of a panic. +* Update `nats-server` version to avoid known zip-slip exploit +* Update `labstack/echo` version to mitigate known open redirect exploit + +### Support Statement +New Relic recommends that you upgrade the agent regularly to ensure that you’re getting the latest features and performance benefits. Additionally, older releases will no longer be supported when they reach end-of-life. + +We also recommend using the latest version of the Go language. At minimum, you should at least be using no version of Go older than what is supported by the Go team themselves. + +See the [Go Agent EOL Policy](https://docs.newrelic.com/docs/apm/agents/go-agent/get-started/go-agent-eol-policy/) for details about supported versions of the Go Agent and third-party components. + ## 3.20.1 ### Added From dff95d15ceba5d4879cd3263f55a3a4a52615057 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Tue, 13 Dec 2022 15:12:13 -0500 Subject: [PATCH 09/10] set app version to 3.20.2 --- v3/newrelic/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3/newrelic/version.go b/v3/newrelic/version.go index d1cc2dfce..afd00ee60 100644 --- a/v3/newrelic/version.go +++ b/v3/newrelic/version.go @@ -11,7 +11,7 @@ import ( const ( // Version is the full string version of this Go Agent. - Version = "3.20.1" + Version = "3.20.2" ) var ( From 4f73195941fb31b8a8446b20635c76f8350293cb Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Wed, 14 Dec 2022 15:21:01 -0500 Subject: [PATCH 10/10] remove direct dependencies from nrecho go mod bump --- v3/integrations/nrecho-v4/go.mod | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/v3/integrations/nrecho-v4/go.mod b/v3/integrations/nrecho-v4/go.mod index f51557781..fd6e3a801 100644 --- a/v3/integrations/nrecho-v4/go.mod +++ b/v3/integrations/nrecho-v4/go.mod @@ -8,19 +8,3 @@ require ( github.com/labstack/echo/v4 v4.9.0 github.com/newrelic/go-agent/v3 v3.18.2 ) - -require ( - github.com/golang/protobuf v1.4.3 // indirect - github.com/labstack/gommon v0.3.1 // indirect - github.com/mattn/go-colorable v0.1.11 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.1 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect - golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect - golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect - google.golang.org/grpc v1.39.0 // indirect - google.golang.org/protobuf v1.25.0 // indirect -)