From a8c6bf874136d1f3f9833bbf68dcbd9868345186 Mon Sep 17 00:00:00 2001 From: Mirac Kara <55501260+mirackara@users.noreply.github.com> Date: Mon, 18 Sep 2023 11:35:03 -0500 Subject: [PATCH 1/3] Release 3.25.0 (#782) * minor fix for complete security disable flag * Create FastHTTP Client Functions * FastHTTP Request Integration * FastHTTP example file * FastHTTP Request Integration * FastHTTP Response file * mod file * update security agent version * supportability metric * Created unit tests and removed extraneous file * Moved FastHTTP to internal instrumentation * Added testing for errors * chore: add logs-in-context example with logrus * chore: move example to specific folder * FastHTTP external segments/Client example * License for Server Example * Added test for external segment/minor fixes * FastHTTP Integration (#774) Added Support For FastHTTP * V3.25.0 Changelog (#781) * V3.25.0 * update version * corrected changelog for 3.25 release * Fixed test not passing * Update segments.go Removed extra function --------- Co-authored-by: aayush-ap Co-authored-by: Steve Willoughby <76975199+nr-swilloughby@users.noreply.github.com> Co-authored-by: Julien Erard Co-authored-by: Emilio Garcia Co-authored-by: Steve Willoughby --- CHANGELOG.md | 17 +++- v3/examples/client-fasthttp/main.go | 62 ++++++++++++ v3/examples/server-fasthttp/main.go | 58 +++++++++++ v3/go.mod | 1 + v3/integrations/nrfasthttp/go.mod | 9 ++ .../server-http-logs-in-context/main.go | 97 +++++++++++++++++++ .../{example => examples/server}/main.go | 0 v3/integrations/nrsecurityagent/go.mod | 2 +- .../nrsecurityagent/nrsecurityagent.go | 2 +- v3/newrelic/context.go | 14 +++ v3/newrelic/instrumentation.go | 91 ++++++++++++++++- v3/newrelic/internal_17_test.go | 43 ++++++++ v3/newrelic/internal_context_test.go | 25 +++++ v3/newrelic/segments.go | 33 +++++++ v3/newrelic/version.go | 2 +- 15 files changed, 447 insertions(+), 9 deletions(-) create mode 100644 v3/examples/client-fasthttp/main.go create mode 100644 v3/examples/server-fasthttp/main.go create mode 100644 v3/integrations/nrfasthttp/go.mod create mode 100644 v3/integrations/nrlogrus/examples/server-http-logs-in-context/main.go rename v3/integrations/nrlogrus/{example => examples/server}/main.go (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5719b313b..92ae88fa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,30 @@ +## 3.25.0 +### Added + * Added Support for FastHTTP package + * Added newrelic.WrapHandleFuncFastHTTP() and newrelic.StartExternalSegmentFastHTTP() functions to instrument fasthttp context and create wrapped handlers. These functions work similarly to the existing ones for net/http + * Added client-fasthttp and server-fasthttp examples to help get started with FastHTTP integration + +### Fixed + * Corrected a bug where the security agent failed to correctly parse the `NEW_RELIC_SECURITY_AGENT_ENABLED` environment variable. + +### Support statement +We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves (i.e., Go versions 1.19 and later are supported). +We recommend updating to the latest agent version as soon as it’s available. If you can’t upgrade to the latest version, update your agents to a version no more than 90 days old. Read more about keeping agents up to date. (https://docs.newrelic.com/docs/new-relic-solutions/new-relic-one/install-configure/update-new-relic-agent/) +See the [Go agent EOL Policy](/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.24.1 ### Fixed * Performance improvement around calls to security agent. In some cases, unnecessary setup operations were being performed even if there was no security agent present to use that. These are now conditional on the security agent being present in the application (note that this will enable the setup code if the security agent is *present* in the application, regardless of whether it's currently enabled to run). This affects: * Base agent code (updated to v3.24.1) * `nrmongo` integration (updated to v1.1.1) + * Resolved a race condition caused by the above-mentioned calls to the security agent. * Fixed unit tests for integrations which were failing because code level metrics are enabled by default now: * `nrawssdk-v1` (updated to v1.1.2) * `nrawssdk-v2` (updated to v1.2.2) * `nrecho-v3` (updated to v1.0.2) * `nrecho-v4` (updated to v1.0.4) - * `nrhttprouter` (updated to + * `nrhttprouter` (updated to v1.0.2) * `nrlambda` (updated to v1.2.2) * `nrnats` (updated to v1.1.5) * `nrredis-v8` (updated to v1.0.1) diff --git a/v3/examples/client-fasthttp/main.go b/v3/examples/client-fasthttp/main.go new file mode 100644 index 000000000..7a26b605f --- /dev/null +++ b/v3/examples/client-fasthttp/main.go @@ -0,0 +1,62 @@ +// Copyright 2020 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "fmt" + "os" + "time" + + newrelic "github.com/newrelic/go-agent/v3/newrelic" + "github.com/valyala/fasthttp" +) + +func doRequest(txn *newrelic.Transaction) error { + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + req.SetRequestURI("http://localhost:8080/hello") + req.Header.SetMethod("GET") + + ctx := &fasthttp.RequestCtx{} + seg := newrelic.StartExternalSegmentFastHTTP(txn, ctx) + defer seg.End() + + err := fasthttp.Do(req, resp) + if err != nil { + return err + } + + fmt.Println("Response Code is ", resp.StatusCode()) + return nil + +} + +func main() { + app, err := newrelic.NewApplication( + newrelic.ConfigAppName("Client App"), + newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), + newrelic.ConfigDebugLogger(os.Stdout), + newrelic.ConfigDistributedTracerEnabled(true), + ) + + if err := app.WaitForConnection(5 * time.Second); nil != err { + fmt.Println(err) + } + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + txn := app.StartTransaction("client-txn") + err = doRequest(txn) + if err != nil { + txn.NoticeError(err) + } + txn.End() + + // Shut down the application to flush data to New Relic. + app.Shutdown(10 * time.Second) +} diff --git a/v3/examples/server-fasthttp/main.go b/v3/examples/server-fasthttp/main.go new file mode 100644 index 000000000..8ed532670 --- /dev/null +++ b/v3/examples/server-fasthttp/main.go @@ -0,0 +1,58 @@ +// Copyright 2020 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "errors" + "fmt" + "os" + "time" + + newrelic "github.com/newrelic/go-agent/v3/newrelic" + + "github.com/valyala/fasthttp" +) + +func index(ctx *fasthttp.RequestCtx) { + ctx.WriteString("Hello World") +} + +func noticeError(ctx *fasthttp.RequestCtx) { + ctx.WriteString("noticing an error") + txn := ctx.UserValue("transaction").(*newrelic.Transaction) + txn.NoticeError(errors.New("my error message")) +} + +func main() { + // Initialize New Relic + app, err := newrelic.NewApplication( + newrelic.ConfigAppName("FastHTTP App"), + newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), + newrelic.ConfigDebugLogger(os.Stdout), + newrelic.ConfigDistributedTracerEnabled(true), + ) + if err != nil { + fmt.Println(err) + return + } + if err := app.WaitForConnection(5 * time.Second); nil != err { + fmt.Println(err) + } + _, helloRoute := newrelic.WrapHandleFuncFastHTTP(app, "/hello", index) + _, errorRoute := newrelic.WrapHandleFuncFastHTTP(app, "/error", noticeError) + handler := func(ctx *fasthttp.RequestCtx) { + path := string(ctx.Path()) + method := string(ctx.Method()) + + switch { + case method == "GET" && path == "/hello": + helloRoute(ctx) + case method == "GET" && path == "/error": + errorRoute(ctx) + } + } + + // Start the server with the instrumented handler + fasthttp.ListenAndServe(":8080", handler) +} diff --git a/v3/go.mod b/v3/go.mod index d00562a5a..910c288d1 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/golang/protobuf v1.5.3 + github.com/valyala/fasthttp v1.49.0 google.golang.org/grpc v1.54.0 ) diff --git a/v3/integrations/nrfasthttp/go.mod b/v3/integrations/nrfasthttp/go.mod new file mode 100644 index 000000000..d4e207230 --- /dev/null +++ b/v3/integrations/nrfasthttp/go.mod @@ -0,0 +1,9 @@ +module github.com/newrelic/go-agent/v3/integrations/nrfasthttp + +go 1.19 + +require ( + github.com/newrelic/go-agent/v3 v3.23.1 + github.com/stretchr/testify v1.8.4 + github.com/valyala/fasthttp v1.48.0 +) diff --git a/v3/integrations/nrlogrus/examples/server-http-logs-in-context/main.go b/v3/integrations/nrlogrus/examples/server-http-logs-in-context/main.go new file mode 100644 index 000000000..7ce3a5782 --- /dev/null +++ b/v3/integrations/nrlogrus/examples/server-http-logs-in-context/main.go @@ -0,0 +1,97 @@ +// Copyright 2020 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// An application that illustrates Distributed Tracing with Logs-in-Context +// when using http.Server or similar frameworks. +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/newrelic/go-agent/v3/integrations/logcontext-v2/nrlogrus" + newrelic "github.com/newrelic/go-agent/v3/newrelic" + "github.com/sirupsen/logrus" +) + +type handler struct { + App *newrelic.Application +} + +func (h *handler) ServeHTTP(writer http.ResponseWriter, req *http.Request) { + // The call to StartTransaction must include the response writer and the + // request. + txn := h.App.StartTransaction("server-txn") + defer txn.End() + + txnLogger := logrus.WithContext(newrelic.NewContext(context.Background(), txn)) + + writer = txn.SetWebResponse(writer) + txn.SetWebRequestHTTP(req) + + if req.URL.String() == "/segments" { + defer txn.StartSegment("f1").End() + + txnLogger.Infof("/segments just started") + + func() { + defer txn.StartSegment("f2").End() + + io.WriteString(writer, "segments!") + time.Sleep(10 * time.Millisecond) + + txnLogger.Infof("segment func just about to complete") + }() + time.Sleep(10 * time.Millisecond) + } else { + // Transaction.WriteHeader has to be used instead of invoking + // WriteHeader on the response writer. + writer.WriteHeader(http.StatusNotFound) + } + txnLogger.Infof("handler completing") +} + +func makeApplication() (*newrelic.Application, error) { + app, err := newrelic.NewApplication( + newrelic.ConfigAppName("HTTP Server App"), + newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")), + ) + if nil != err { + return nil, err + } + nrlogrusFormatter := nrlogrus.NewFormatter(app, &logrus.TextFormatter{}) + logrus.SetFormatter(nrlogrusFormatter) + // Alternatively and if preferred, create a new logger and use that logger + // for logging with + // log := logrus.New() + // log.SetFormatter(nrlogrusFormatter) + + // Wait for the application to connect. + if err = app.WaitForConnection(5 * time.Second); nil != err { + return nil, err + } + + return app, nil +} + +func main() { + + app, err := makeApplication() + if nil != err { + fmt.Println(err) + os.Exit(1) + } + + logrus.Infof("Application Starting") + + server := http.Server{ + Addr: ":8000", + Handler: &handler{App: app}, + } + + server.ListenAndServe() +} diff --git a/v3/integrations/nrlogrus/example/main.go b/v3/integrations/nrlogrus/examples/server/main.go similarity index 100% rename from v3/integrations/nrlogrus/example/main.go rename to v3/integrations/nrlogrus/examples/server/main.go diff --git a/v3/integrations/nrsecurityagent/go.mod b/v3/integrations/nrsecurityagent/go.mod index 2e805f3b8..990ec08c0 100644 --- a/v3/integrations/nrsecurityagent/go.mod +++ b/v3/integrations/nrsecurityagent/go.mod @@ -3,7 +3,7 @@ module github.com/newrelic/go-agent/v3/integrations/nrsecurityagent go 1.19 require ( - github.com/newrelic/csec-go-agent v0.3.0 + github.com/newrelic/csec-go-agent v0.4.0 github.com/newrelic/go-agent/v3 v3.24.1 github.com/newrelic/go-agent/v3/integrations/nrsqlite3 v1.2.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/v3/integrations/nrsecurityagent/nrsecurityagent.go b/v3/integrations/nrsecurityagent/nrsecurityagent.go index acc994eca..c7264d7ad 100644 --- a/v3/integrations/nrsecurityagent/nrsecurityagent.go +++ b/v3/integrations/nrsecurityagent/nrsecurityagent.go @@ -37,7 +37,7 @@ func defaultSecurityConfig() SecurityConfig { // If env is set to false,the security module is not loaded func isSecurityAgentEnabled() bool { if env := os.Getenv("NEW_RELIC_SECURITY_AGENT_ENABLED"); env != "" { - if b, err := strconv.ParseBool("false"); err == nil { + if b, err := strconv.ParseBool(env); err == nil { return b } } diff --git a/v3/newrelic/context.go b/v3/newrelic/context.go index 5ce186f3d..731dcb73f 100644 --- a/v3/newrelic/context.go +++ b/v3/newrelic/context.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/newrelic/go-agent/v3/internal" + "github.com/valyala/fasthttp" ) // NewContext returns a new context.Context that carries the provided @@ -52,3 +53,16 @@ func transactionFromRequestContext(req *http.Request) *Transaction { } return txn } + +func transactionFromRequestContextFastHTTP(ctx *fasthttp.RequestCtx) *Transaction { + var txn *Transaction + if nil != ctx { + txn := ctx.UserValue("transaction").(*Transaction) + return txn + } + + if txn != nil { + return txn + } + return nil +} diff --git a/v3/newrelic/instrumentation.go b/v3/newrelic/instrumentation.go index 4e37e5316..e4351a955 100644 --- a/v3/newrelic/instrumentation.go +++ b/v3/newrelic/instrumentation.go @@ -5,18 +5,41 @@ package newrelic import ( "net/http" + + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpadaptor" ) +type fasthttpWrapperResponse struct { + ctx *fasthttp.RequestCtx +} + +func (rw fasthttpWrapperResponse) Header() http.Header { + hdrs := http.Header{} + rw.ctx.Request.Header.VisitAll(func(key, value []byte) { + hdrs.Add(string(key), string(value)) + }) + return hdrs +} + +func (rw fasthttpWrapperResponse) Write(b []byte) (int, error) { + return rw.ctx.Write(b) +} + +func (rw fasthttpWrapperResponse) WriteHeader(code int) { + rw.ctx.SetStatusCode(code) +} + // instrumentation.go contains helpers built on the lower level api. // WrapHandle instruments http.Handler handlers with Transactions. To // instrument this code: // -// http.Handle("/foo", myHandler) +// http.Handle("/foo", myHandler) // // Perform this replacement: // -// http.Handle(newrelic.WrapHandle(app, "/foo", myHandler)) +// http.Handle(newrelic.WrapHandle(app, "/foo", myHandler)) // // WrapHandle adds the Transaction to the request's context. Access it using // FromContext to add attributes, create segments, or notice errors: @@ -76,6 +99,56 @@ func WrapHandle(app *Application, pattern string, handler http.Handler, options }) } +func WrapHandleFastHTTP(app *Application, pattern string, handler fasthttp.RequestHandler, options ...TraceOption) (string, fasthttp.RequestHandler) { + if app == nil { + return pattern, handler + } + + // add the wrapped function to the trace options as the source code reference point + // (but only if we know we're collecting CLM for this transaction and the user didn't already + // specify a different code location explicitly). + cache := NewCachedCodeLocation() + + return pattern, func(ctx *fasthttp.RequestCtx) { + var tOptions *traceOptSet + var txnOptionList []TraceOption + + if app.app != nil && app.app.run != nil && app.app.run.Config.CodeLevelMetrics.Enabled { + tOptions = resolveCLMTraceOptions(options) + if tOptions != nil && !tOptions.SuppressCLM && (tOptions.DemandCLM || app.app.run.Config.CodeLevelMetrics.Scope == 0 || (app.app.run.Config.CodeLevelMetrics.Scope&TransactionCLM) != 0) { + // we are for sure collecting CLM here, so go to the trouble of collecting this code location if nothing else has yet. + if tOptions.LocationOverride == nil { + if loc, err := cache.FunctionLocation(handler); err == nil { + WithCodeLocation(loc)(tOptions) + } + } + } + } + if tOptions == nil { + // we weren't able to curate the options above, so pass whatever we were given downstream + txnOptionList = options + } else { + txnOptionList = append(txnOptionList, withPreparedOptions(tOptions)) + } + + method := string(ctx.Method()) + path := string(ctx.Path()) + txn := app.StartTransaction(method+" "+path, txnOptionList...) + ctx.SetUserValue("transaction", txn) + defer txn.End() + r := &http.Request{} + fasthttpadaptor.ConvertRequest(ctx, r, true) + resp := fasthttpWrapperResponse{ctx: ctx} + + txn.SetWebResponse(resp) + txn.SetWebRequestHTTP(r) + + r = RequestWithTransactionContext(r, txn) + + handler(ctx) + } +} + // WrapHandleFunc instruments handler functions using Transactions. To // instrument this code: // @@ -111,15 +184,23 @@ func WrapHandleFunc(app *Application, pattern string, handler func(http.Response return p, func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r) } } -// +func WrapHandleFuncFastHTTP(app *Application, pattern string, handler func(*fasthttp.RequestCtx), options ...TraceOption) (string, func(*fasthttp.RequestCtx)) { + // add the wrapped function to the trace options as the source code reference point + // (to the beginning of the option list, so that the user can override this) + + p, h := WrapHandleFastHTTP(app, pattern, fasthttp.RequestHandler(handler), options...) + return p, func(ctx *fasthttp.RequestCtx) { h(ctx) } +} + // WrapListen wraps an HTTP endpoint reference passed to functions like http.ListenAndServe, // which causes security scanning to be done for that incoming endpoint when vulnerability // scanning is enabled. It returns the endpoint string, so you can replace a call like // -// http.ListenAndServe(":8000", nil) +// http.ListenAndServe(":8000", nil) +// // with -// http.ListenAndServe(newrelic.WrapListen(":8000"), nil) // +// http.ListenAndServe(newrelic.WrapListen(":8000"), nil) func WrapListen(endpoint string) string { if IsSecurityAgentPresent() { secureAgent.SendEvent("APP_INFO", endpoint) diff --git a/v3/newrelic/internal_17_test.go b/v3/newrelic/internal_17_test.go index 82d1dc8f1..5ba7b6c7e 100644 --- a/v3/newrelic/internal_17_test.go +++ b/v3/newrelic/internal_17_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/newrelic/go-agent/v3/internal" + "github.com/valyala/fasthttp" ) func myErrorHandler(w http.ResponseWriter, req *http.Request) { @@ -18,6 +19,48 @@ func myErrorHandler(w http.ResponseWriter, req *http.Request) { txn.NoticeError(myError{}) } +func myErrorHandlerFastHTTP(ctx *fasthttp.RequestCtx) { + ctx.WriteString("noticing an error") + txn := ctx.UserValue("transaction").(*Transaction) + txn.NoticeError(myError{}) +} + +func TestWrapHandleFastHTTPFunc(t *testing.T) { + app := testApp(nil, ConfigDistributedTracerEnabled(true), t) + + _, wrappedHandler := WrapHandleFuncFastHTTP(app.Application, "/hello", myErrorHandlerFastHTTP) + + if wrappedHandler == nil { + t.Error("Error when creating a wrapped handler") + } + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.SetRequestURI("/hello") + wrappedHandler(ctx) + app.ExpectErrors(t, []internal.WantError{{ + TxnName: "WebTransaction/Go/GET /hello", + Msg: "my msg", + Klass: "newrelic.myError", + }}) + + app.ExpectMetrics(t, []internal.WantMetric{ + {Name: "WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: nil}, + {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, + {Name: "WebTransactionTotalTime/Go/GET /hello", Scope: "", Forced: false, Data: nil}, + {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, + {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, + {Name: "Apdex", Scope: "", Forced: true, Data: nil}, + {Name: "Apdex/Go/GET /hello", Scope: "", Forced: false, Data: nil}, + {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, + {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, + {Name: "Errors/all", Scope: "", Forced: true, Data: singleCount}, + {Name: "Errors/allWeb", Scope: "", Forced: true, Data: singleCount}, + {Name: "Errors/WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: singleCount}, + {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, + {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, + }) +} + func TestWrapHandleFunc(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) mux := http.NewServeMux() diff --git a/v3/newrelic/internal_context_test.go b/v3/newrelic/internal_context_test.go index 1e15e61cd..51372d382 100644 --- a/v3/newrelic/internal_context_test.go +++ b/v3/newrelic/internal_context_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/newrelic/go-agent/v3/internal" + "github.com/valyala/fasthttp" ) func TestWrapHandlerContext(t *testing.T) { @@ -36,6 +37,30 @@ func TestWrapHandlerContext(t *testing.T) { {Name: "Custom/mySegment", Scope: scope, Forced: false, Data: nil}, }) } +func TestExternalSegmentFastHTTP(t *testing.T) { + app := testApp(nil, ConfigDistributedTracerEnabled(false), t) + txn := app.StartTransaction("myTxn") + + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + req.SetRequestURI("http://localhost:8080/hello") + req.Header.SetMethod("GET") + + ctx := &fasthttp.RequestCtx{} + seg := StartExternalSegmentFastHTTP(txn, ctx) + defer seg.End() + + txn.End() + app.ExpectMetrics(t, []internal.WantMetric{ + {Name: "OtherTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, + {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, + {Name: "OtherTransactionTotalTime/Go/myTxn", Scope: "", Forced: false, Data: nil}, + {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, + }) +} func TestStartExternalSegmentNilTransaction(t *testing.T) { // Test that StartExternalSegment pulls the transaction from the diff --git a/v3/newrelic/segments.go b/v3/newrelic/segments.go index 91f8fcc5a..65344033a 100644 --- a/v3/newrelic/segments.go +++ b/v3/newrelic/segments.go @@ -5,6 +5,9 @@ package newrelic import ( "net/http" + + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpadaptor" ) // SegmentStartTime is created by Transaction.StartSegmentNow and marks the @@ -337,6 +340,36 @@ func StartExternalSegment(txn *Transaction, request *http.Request) *ExternalSegm return s } +func StartExternalSegmentFastHTTP(txn *Transaction, ctx *fasthttp.RequestCtx) *ExternalSegment { + if nil == txn { + txn = transactionFromRequestContextFastHTTP(ctx) + } + request := &http.Request{} + + fasthttpadaptor.ConvertRequest(ctx, request, true) + s := &ExternalSegment{ + StartTime: txn.StartSegmentNow(), + Request: request, + } + if IsSecurityAgentPresent() { + s.secureAgentEvent = secureAgent.SendEvent("OUTBOUND", request) + } + + if request != nil && request.Header != nil { + for key, values := range s.outboundHeaders() { + for _, value := range values { + request.Header.Set(key, value) + } + } + + if IsSecurityAgentPresent() { + secureAgent.DistributedTraceHeaders(request, s.secureAgentEvent) + } + } + + return s +} + func addSpanAttr(start SegmentStartTime, key string, val interface{}) { if nil == start.thread { return diff --git a/v3/newrelic/version.go b/v3/newrelic/version.go index b1a8adefd..c6b63256f 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.24.1" + Version = "3.25.0" ) var ( From 0e521dc894ec887e31125067f28bd3992cf33310 Mon Sep 17 00:00:00 2001 From: Emilio Garcia Date: Thu, 16 Nov 2023 14:22:37 -0500 Subject: [PATCH 2/3] Release 3.28.0 (#818) * Error Expected Bug The attribute error.expected should be a boolean, not a string. It is also good practice to use a constant value for the key. * Bump google.golang.org/grpc from 1.54.0 to 1.56.3 in /v3/integrations/nrgraphqlgo/example (#811) --------- * Bump google.golang.org/grpc in /v3/integrations/nrgraphqlgo/example Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.54.0 to 1.56.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.54.0...v1.56.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: indirect ... Signed-off-by: dependabot[bot] --------- * Bump google.golang.org/grpc from 1.54.0 to 1.56.3 in /v3/integrations/nrgrpc (#810) --------- * Bump google.golang.org/grpc in /v3/integrations/nrgrpc Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.54.0 to 1.56.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.54.0...v1.56.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production ... Signed-off-by: dependabot[bot] --------- * Bump google.golang.org/grpc from 1.54.0 to 1.56.3 in /v3 (#809) --------- * Bump google.golang.org/grpc from 1.54.0 to 1.56.3 in /v3 Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.54.0 to 1.56.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.54.0...v1.56.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production ... Signed-off-by: dependabot[bot] --------- * Bump golang.org/x/net from 0.8.0 to 0.17.0 in /v3/integrations/nrgraphqlgo/example (#804) --------- * Bump golang.org/x/net in /v3/integrations/nrgraphqlgo/example Bumps [golang.org/x/net](https://github.com/golang/net) from 0.8.0 to 0.17.0. - [Commits](https://github.com/golang/net/compare/v0.8.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] --------- * Fix for out of memory error with request body (#806) * Release 3.25.0 (#782) * minor fix for complete security disable flag * Create FastHTTP Client Functions * FastHTTP Request Integration * FastHTTP example file * FastHTTP Request Integration * FastHTTP Response file * mod file * update security agent version * supportability metric * Created unit tests and removed extraneous file * Moved FastHTTP to internal instrumentation * Added testing for errors * chore: add logs-in-context example with logrus * chore: move example to specific folder * FastHTTP external segments/Client example * License for Server Example * Added test for external segment/minor fixes * FastHTTP Integration (#774) Added Support For FastHTTP * V3.25.0 Changelog (#781) * V3.25.0 * update version * corrected changelog for 3.25 release * Fixed test not passing * Update segments.go Removed extra function --------- Co-authored-by: aayush-ap Co-authored-by: Steve Willoughby <76975199+nr-swilloughby@users.noreply.github.com> Co-authored-by: Julien Erard Co-authored-by: Emilio Garcia Co-authored-by: Steve Willoughby * fix out of memory issue for req body * Added new config parameter for read request body * update request body buffer * minor fix for dataTruncated * Update readme file * Update csec-go-agent version * Added new wrapper for go-micro stream server * minor fix for GHA * Fix for cpu overhead * backward compatibility * update agent version * minor fix --------- Co-authored-by: Mirac Kara <55501260+mirackara@users.noreply.github.com> Co-authored-by: Steve Willoughby <76975199+nr-swilloughby@users.noreply.github.com> Co-authored-by: Julien Erard Co-authored-by: Emilio Garcia Co-authored-by: Steve Willoughby * move fasthttp out of core library, and into integration package (#808) * move fasthttp out of core library, and into integration package * move examples over * add security agent headers to fasthttp object * fix examples and external segment * add fasthttp tests * cleanup of go mods * fix segment collection * add security agent inbound write capture to wrapped handle func * Update go.mod * Update Changelog * update version.go --------- Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: aayush-ap <59004877+aayush-ap@users.noreply.github.com> Co-authored-by: Mirac Kara <55501260+mirackara@users.noreply.github.com> Co-authored-by: Steve Willoughby <76975199+nr-swilloughby@users.noreply.github.com> Co-authored-by: Julien Erard Co-authored-by: Steve Willoughby Co-authored-by: mirackara --- .github/workflows/ci.yaml | 2 + CHANGELOG.md | 15 +++ v3/go.mod | 3 +- .../examples/client-fasthttp/go.mod | 11 +++ .../examples/client-fasthttp/main.go | 4 +- .../examples/server-fasthttp/go.mod | 11 +++ .../examples/server-fasthttp/main.go | 7 +- v3/integrations/nrfasthttp/go.mod | 6 +- v3/integrations/nrfasthttp/instrumentation.go | 74 ++++++++++++++ .../nrfasthttp/instrumentation_test.go | 56 +++++++++++ v3/integrations/nrfasthttp/segment.go | 80 +++++++++++++++ v3/integrations/nrfasthttp/segment_test.go | 65 ++++++++++++ v3/integrations/nrgraphqlgo/example/go.mod | 12 +-- v3/integrations/nrgrpc/go.mod | 6 +- v3/integrations/nrmicro/nrmicro.go | 72 +++++++++++--- v3/integrations/nrsecurityagent/README.md | 2 + v3/integrations/nrsecurityagent/go.mod | 2 +- .../nrsecurityagent/nrsecurityagent.go | 21 ++++ v3/newrelic/context.go | 14 --- v3/newrelic/error_events.go | 2 +- v3/newrelic/instrumentation.go | 98 +++++-------------- v3/newrelic/internal_17_test.go | 43 -------- v3/newrelic/internal_context_test.go | 25 ----- v3/newrelic/secure_agent.go | 58 +++++++++-- v3/newrelic/segments.go | 50 ++++------ v3/newrelic/transaction.go | 35 ++++--- v3/newrelic/version.go | 2 +- 27 files changed, 532 insertions(+), 244 deletions(-) create mode 100644 v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod rename v3/{ => integrations/nrfasthttp}/examples/client-fasthttp/main.go (92%) create mode 100644 v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod rename v3/{ => integrations/nrfasthttp}/examples/server-fasthttp/main.go (83%) create mode 100644 v3/integrations/nrfasthttp/instrumentation.go create mode 100644 v3/integrations/nrfasthttp/instrumentation_test.go create mode 100644 v3/integrations/nrfasthttp/segment.go create mode 100644 v3/integrations/nrfasthttp/segment_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f35e91398..6b5636a35 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -27,6 +27,8 @@ jobs: # v3 integrations - go-version: 1.19.x dirs: v3/integrations/nramqp + - go-version: 1.19.x + dirs: v3/integrations/nrfasthttp - go-version: 1.19.x dirs: v3/integrations/nrsarama - go-version: 1.19.x diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c696edb3..366e2ac6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 3.28.0 +### Fixed +* Bumped gRPC from 1.54.0 -> 1.56.3 in the following packages /v3/integrations/nrgrpc, /v3/, /v3/integrations/nrgrpc +* Bumped golang.org/x/net from 0.8.0 -> 0.17.0 in package /v3/integrations/nrgraphqlgo +* Fixed issue where nrfasthttp would not properly register security agent headers +* Move fasthttp instrumentation into a new integration package, nrfasthttp +* Fixed issue where usage of io.ReadAll() was causing a memory leak + +### Support statement + +We use the latest version of the Go language. At minimum, you should be using no version of Go older than what is supported by the Go team themselves. + +See the [Go agent EOL Policy](/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.27.0 ### Added * Added Support for getting Container ID's from cgroup v2 docker containers diff --git a/v3/go.mod b/v3/go.mod index 02c244f6b..e064d1ccf 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -4,8 +4,7 @@ go 1.19 require ( github.com/golang/protobuf v1.5.3 - github.com/valyala/fasthttp v1.49.0 - google.golang.org/grpc v1.54.0 + google.golang.org/grpc v1.56.3 ) retract v3.22.0 // release process error corrected in v3.22.1 diff --git a/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod b/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod new file mode 100644 index 000000000..104505e67 --- /dev/null +++ b/v3/integrations/nrfasthttp/examples/client-fasthttp/go.mod @@ -0,0 +1,11 @@ +module client-example + +go 1.19 + +require ( + github.com/newrelic/go-agent/v3 v3.28.0 + github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 + github.com/valyala/fasthttp v1.49.0 +) + +replace github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 => ../../ diff --git a/v3/examples/client-fasthttp/main.go b/v3/integrations/nrfasthttp/examples/client-fasthttp/main.go similarity index 92% rename from v3/examples/client-fasthttp/main.go rename to v3/integrations/nrfasthttp/examples/client-fasthttp/main.go index 7a26b605f..bb958b840 100644 --- a/v3/examples/client-fasthttp/main.go +++ b/v3/integrations/nrfasthttp/examples/client-fasthttp/main.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/newrelic/go-agent/v3/integrations/nrfasthttp" newrelic "github.com/newrelic/go-agent/v3/newrelic" "github.com/valyala/fasthttp" ) @@ -20,8 +21,7 @@ func doRequest(txn *newrelic.Transaction) error { req.SetRequestURI("http://localhost:8080/hello") req.Header.SetMethod("GET") - ctx := &fasthttp.RequestCtx{} - seg := newrelic.StartExternalSegmentFastHTTP(txn, ctx) + seg := nrfasthttp.StartExternalSegment(txn, req) defer seg.End() err := fasthttp.Do(req, resp) diff --git a/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod b/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod new file mode 100644 index 000000000..591d85885 --- /dev/null +++ b/v3/integrations/nrfasthttp/examples/server-fasthttp/go.mod @@ -0,0 +1,11 @@ +module server-example + +go 1.19 + +require ( + github.com/newrelic/go-agent/v3 v3.28.0 + github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 + github.com/valyala/fasthttp v1.49.0 +) + +replace github.com/newrelic/go-agent/v3/integrations/nrfasthttp v1.0.0 => ../../ diff --git a/v3/examples/server-fasthttp/main.go b/v3/integrations/nrfasthttp/examples/server-fasthttp/main.go similarity index 83% rename from v3/examples/server-fasthttp/main.go rename to v3/integrations/nrfasthttp/examples/server-fasthttp/main.go index 8ed532670..bdb642f85 100644 --- a/v3/examples/server-fasthttp/main.go +++ b/v3/integrations/nrfasthttp/examples/server-fasthttp/main.go @@ -9,7 +9,8 @@ import ( "os" "time" - newrelic "github.com/newrelic/go-agent/v3/newrelic" + "github.com/newrelic/go-agent/v3/integrations/nrfasthttp" + "github.com/newrelic/go-agent/v3/newrelic" "github.com/valyala/fasthttp" ) @@ -39,8 +40,8 @@ func main() { if err := app.WaitForConnection(5 * time.Second); nil != err { fmt.Println(err) } - _, helloRoute := newrelic.WrapHandleFuncFastHTTP(app, "/hello", index) - _, errorRoute := newrelic.WrapHandleFuncFastHTTP(app, "/error", noticeError) + _, helloRoute := nrfasthttp.WrapHandleFunc(app, "/hello", index) + _, errorRoute := nrfasthttp.WrapHandleFunc(app, "/error", noticeError) handler := func(ctx *fasthttp.RequestCtx) { path := string(ctx.Path()) method := string(ctx.Method()) diff --git a/v3/integrations/nrfasthttp/go.mod b/v3/integrations/nrfasthttp/go.mod index 7f2b81134..5c97651a0 100644 --- a/v3/integrations/nrfasthttp/go.mod +++ b/v3/integrations/nrfasthttp/go.mod @@ -3,8 +3,6 @@ module github.com/newrelic/go-agent/v3/integrations/nrfasthttp go 1.19 require ( - github.com/newrelic/go-agent/v3 v3.26.0 - github.com/stretchr/testify v1.8.4 - github.com/valyala/fasthttp v1.48.0 + github.com/newrelic/go-agent/v3 v3.28.0 + github.com/valyala/fasthttp v1.49.0 ) -replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrfasthttp/instrumentation.go b/v3/integrations/nrfasthttp/instrumentation.go new file mode 100644 index 000000000..9ce64769c --- /dev/null +++ b/v3/integrations/nrfasthttp/instrumentation.go @@ -0,0 +1,74 @@ +package nrfasthttp + +import ( + "net/http" + + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpadaptor" +) + +type fasthttpWrapperResponse struct { + ctx *fasthttp.RequestCtx +} + +func (rw fasthttpWrapperResponse) Header() http.Header { + hdrs := http.Header{} + rw.ctx.Request.Header.VisitAll(func(key, value []byte) { + hdrs.Add(string(key), string(value)) + }) + return hdrs +} + +func (rw fasthttpWrapperResponse) Write(b []byte) (int, error) { + return rw.ctx.Write(b) +} + +func (rw fasthttpWrapperResponse) WriteHeader(code int) { + rw.ctx.SetStatusCode(code) +} + +func (rw fasthttpWrapperResponse) Body() string { + body := rw.ctx.Response.Body() + return string(body) +} + +// WrapHandleFunc wrapps a fasthttp handler function for automatic instrumentation +func WrapHandleFunc(app *newrelic.Application, pattern string, handler func(*fasthttp.RequestCtx), options ...newrelic.TraceOption) (string, func(*fasthttp.RequestCtx)) { + // add the wrapped function to the trace options as the source code reference point + // (to the beginning of the option list, so that the user can override this) + + p, h := WrapHandle(app, pattern, fasthttp.RequestHandler(handler), options...) + return p, func(ctx *fasthttp.RequestCtx) { h(ctx) } +} + +// WrapHandle wraps a fasthttp request handler for automatic instrumentation +func WrapHandle(app *newrelic.Application, pattern string, handler fasthttp.RequestHandler, options ...newrelic.TraceOption) (string, fasthttp.RequestHandler) { + if app == nil { + return pattern, handler + } + + // add the wrapped function to the trace options as the source code reference point + // (but only if we know we're collecting CLM for this transaction and the user didn't already + // specify a different code location explicitly). + return pattern, func(ctx *fasthttp.RequestCtx) { + cache := newrelic.NewCachedCodeLocation() + txnOptionList := newrelic.AddCodeLevelMetricsTraceOptions(app, options, cache, handler) + method := string(ctx.Method()) + path := string(ctx.Path()) + txn := app.StartTransaction(method+" "+path, txnOptionList...) + ctx.SetUserValue("transaction", txn) + defer txn.End() + r := &http.Request{} + fasthttpadaptor.ConvertRequest(ctx, r, true) + resp := fasthttpWrapperResponse{ctx: ctx} + + txn.SetWebResponse(resp) + txn.SetWebRequestHTTP(r) + + if newrelic.IsSecurityAgentPresent() { + newrelic.GetSecurityAgentInterface().SendEvent("INBOUND_WRITE", resp.Body(), resp.Header()) + } + handler(ctx) + } +} diff --git a/v3/integrations/nrfasthttp/instrumentation_test.go b/v3/integrations/nrfasthttp/instrumentation_test.go new file mode 100644 index 000000000..844c3682b --- /dev/null +++ b/v3/integrations/nrfasthttp/instrumentation_test.go @@ -0,0 +1,56 @@ +package nrfasthttp + +import ( + "testing" + + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/valyala/fasthttp" +) + +type myError struct{} + +func (e myError) Error() string { return "my msg" } + +func myErrorHandlerFastHTTP(ctx *fasthttp.RequestCtx) { + ctx.WriteString("noticing an error") + txn := ctx.UserValue("transaction").(*newrelic.Transaction) + txn.NoticeError(myError{}) +} + +func TestWrapHandleFastHTTPFunc(t *testing.T) { + singleCount := []float64{1, 0, 0, 0, 0, 0, 0} + app := createTestApp(true) + + _, wrappedHandler := WrapHandleFunc(app.Application, "/hello", myErrorHandlerFastHTTP) + + if wrappedHandler == nil { + t.Error("Error when creating a wrapped handler") + } + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod("GET") + ctx.Request.SetRequestURI("/hello") + wrappedHandler(ctx) + app.ExpectErrors(t, []internal.WantError{{ + TxnName: "WebTransaction/Go/GET /hello", + Msg: "my msg", + Klass: "nrfasthttp.myError", + }}) + + app.ExpectMetrics(t, []internal.WantMetric{ + {Name: "WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: nil}, + {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, + {Name: "WebTransactionTotalTime/Go/GET /hello", Scope: "", Forced: false, Data: nil}, + {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, + {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, + {Name: "Apdex", Scope: "", Forced: true, Data: nil}, + {Name: "Apdex/Go/GET /hello", Scope: "", Forced: false, Data: nil}, + {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, + {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, + {Name: "Errors/all", Scope: "", Forced: true, Data: singleCount}, + {Name: "Errors/allWeb", Scope: "", Forced: true, Data: singleCount}, + {Name: "Errors/WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: singleCount}, + {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, + {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, + }) +} diff --git a/v3/integrations/nrfasthttp/segment.go b/v3/integrations/nrfasthttp/segment.go new file mode 100644 index 000000000..aa480f5f0 --- /dev/null +++ b/v3/integrations/nrfasthttp/segment.go @@ -0,0 +1,80 @@ +package nrfasthttp + +import ( + "net/http" + + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpadaptor" +) + +// StartExternalSegment automatically creates and fills out a New Relic external segment for a given +// fasthttp request object. This function will accept either a fasthttp.Request or a fasthttp.RequestContext +// object as the request argument. +func StartExternalSegment(txn *newrelic.Transaction, request any) *newrelic.ExternalSegment { + var secureAgentEvent any + var ctx *fasthttp.RequestCtx + + switch reqObject := request.(type) { + + case *fasthttp.RequestCtx: + ctx = reqObject + + case *fasthttp.Request: + ctx = &fasthttp.RequestCtx{} + reqObject.CopyTo(&ctx.Request) + + default: + return nil + } + + if nil == txn { + txn = transactionFromRequestContext(ctx) + } + req := &http.Request{} + + fasthttpadaptor.ConvertRequest(ctx, req, true) + s := &newrelic.ExternalSegment{ + StartTime: txn.StartSegmentNow(), + Request: req, + } + + if newrelic.IsSecurityAgentPresent() { + secureAgentEvent = newrelic.GetSecurityAgentInterface().SendEvent("OUTBOUND", request) + s.SetSecureAgentEvent(secureAgentEvent) + } + + if request != nil && req.Header != nil { + for key, values := range s.GetOutboundHeaders() { + for _, value := range values { + req.Header.Set(key, value) + } + } + + if newrelic.IsSecurityAgentPresent() { + newrelic.GetSecurityAgentInterface().DistributedTraceHeaders(req, secureAgentEvent) + } + + for k, values := range req.Header { + for _, value := range values { + ctx.Request.Header.Set(k, value) + } + } + } + + return s +} + +// FromContext extracts a transaction pointer from a fasthttp.RequestContext object +func FromContext(ctx *fasthttp.RequestCtx) *newrelic.Transaction { + return transactionFromRequestContext(ctx) +} + +func transactionFromRequestContext(ctx *fasthttp.RequestCtx) *newrelic.Transaction { + if nil != ctx { + txn := ctx.UserValue("transaction").(*newrelic.Transaction) + return txn + } + + return nil +} diff --git a/v3/integrations/nrfasthttp/segment_test.go b/v3/integrations/nrfasthttp/segment_test.go new file mode 100644 index 000000000..550e39a86 --- /dev/null +++ b/v3/integrations/nrfasthttp/segment_test.go @@ -0,0 +1,65 @@ +package nrfasthttp + +import ( + "testing" + + "github.com/newrelic/go-agent/v3/internal" + "github.com/newrelic/go-agent/v3/internal/integrationsupport" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/valyala/fasthttp" +) + +func createTestApp(dt bool) integrationsupport.ExpectApp { + return integrationsupport.NewTestApp(replyFn, integrationsupport.ConfigFullTraces, newrelic.ConfigDistributedTracerEnabled(dt)) +} + +var replyFn = func(reply *internal.ConnectReply) { + reply.SetSampleEverything() +} + +func TestExternalSegment(t *testing.T) { + app := createTestApp(false) + txn := app.StartTransaction("myTxn") + + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseResponse(resp) + + ctx := &fasthttp.RequestCtx{Request: fasthttp.Request{}} + ctx.Request.SetRequestURI("http://localhost:8080/hello") + ctx.Request.Header.SetMethod("GET") + + seg := StartExternalSegment(txn, ctx) + defer seg.End() + + txn.End() + app.ExpectMetrics(t, []internal.WantMetric{ + {Name: "OtherTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, + {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, + {Name: "OtherTransactionTotalTime/Go/myTxn", Scope: "", Forced: false, Data: nil}, + {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, + }) +} + +func TestExternalSegmentRequest(t *testing.T) { + app := createTestApp(false) + txn := app.StartTransaction("myTxn") + + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + req.SetRequestURI("http://localhost:8080/hello") + req.Header.SetMethod("GET") + + seg := StartExternalSegment(txn, req) + defer seg.End() + + txn.End() + app.ExpectMetrics(t, []internal.WantMetric{ + {Name: "OtherTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, + {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, + {Name: "OtherTransactionTotalTime/Go/myTxn", Scope: "", Forced: false, Data: nil}, + {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, + }) +} diff --git a/v3/integrations/nrgraphqlgo/example/go.mod b/v3/integrations/nrgraphqlgo/example/go.mod index d5a044811..ac6bc56ad 100644 --- a/v3/integrations/nrgraphqlgo/example/go.mod +++ b/v3/integrations/nrgraphqlgo/example/go.mod @@ -16,12 +16,12 @@ require ( github.com/klauspost/compress v1.16.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.49.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect - google.golang.org/grpc v1.54.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc v1.56.3 // indirect + google.golang.org/protobuf v1.30.0 // indirect ) replace github.com/newrelic/go-agent/v3/integrations/nrgraphqlgo => ../ diff --git a/v3/integrations/nrgrpc/go.mod b/v3/integrations/nrgrpc/go.mod index 738fa628f..642db88de 100644 --- a/v3/integrations/nrgrpc/go.mod +++ b/v3/integrations/nrgrpc/go.mod @@ -9,9 +9,9 @@ require ( github.com/newrelic/go-agent/v3 v3.26.0 github.com/newrelic/go-agent/v3/integrations/nrsecurityagent v1.1.0 // v1.15.0 is the earliest version of grpc using modules. - google.golang.org/grpc v1.54.0 - google.golang.org/protobuf v1.28.1 + google.golang.org/grpc v1.56.3 + google.golang.org/protobuf v1.30.0 ) - replace github.com/newrelic/go-agent/v3 => ../.. +replace github.com/newrelic/go-agent/v3/integrations/nrsecurityagent => ../../integrations/nrsecurityagent diff --git a/v3/integrations/nrmicro/nrmicro.go b/v3/integrations/nrmicro/nrmicro.go index 7198a81fb..804e531ff 100644 --- a/v3/integrations/nrmicro/nrmicro.go +++ b/v3/integrations/nrmicro/nrmicro.go @@ -5,6 +5,7 @@ package nrmicro import ( "context" + "io" "net/http" "net/url" "strings" @@ -15,9 +16,11 @@ import ( "github.com/micro/go-micro/registry" "github.com/micro/go-micro/server" + protoV1 "github.com/golang/protobuf/proto" "github.com/newrelic/go-agent/v3/internal" "github.com/newrelic/go-agent/v3/internal/integrationsupport" "github.com/newrelic/go-agent/v3/newrelic" + protoV2 "google.golang.org/protobuf/proto" ) type nrWrapper struct { @@ -162,7 +165,19 @@ func HandlerWrapper(app *newrelic.Application) server.HandlerWrapper { return func(ctx context.Context, req server.Request, rsp interface{}) error { txn := startWebTransaction(ctx, app, req) defer txn.End() - err := fn(newrelic.NewContext(ctx, txn), req, rsp) + if req.Body() != nil && newrelic.IsSecurityAgentPresent() { + messageType, version := getMessageType(req.Body()) + newrelic.GetSecurityAgentInterface().SendEvent("GRPC", req.Body(), messageType, version) + } + + nrrsp := rsp + if req.Stream() && newrelic.IsSecurityAgentPresent() { + if stream, ok := rsp.(server.Stream); ok { + nrrsp = wrappedServerStream{stream} + } + } + + err := fn(newrelic.NewContext(ctx, txn), req, nrrsp) var code int if err != nil { if t, ok := err.(*errors.Error); ok { @@ -227,9 +242,6 @@ func SubscriberWrapper(app *newrelic.Application) server.SubscriberWrapper { func startWebTransaction(ctx context.Context, app *newrelic.Application, req server.Request) *newrelic.Transaction { var hdrs http.Header - var unencodedBody []byte - var err error - if md, ok := metadata.FromContext(ctx); ok { hdrs = make(http.Header, len(md)) for k, v := range md { @@ -242,20 +254,58 @@ func startWebTransaction(ctx context.Context, app *newrelic.Application, req ser Host: req.Service(), Path: req.Endpoint(), } - - if unencodedBody, err = req.Read(); err != nil { - unencodedBody = nil - } - webReq := newrelic.WebRequest{ Header: hdrs, URL: u, Method: req.Method(), Transport: newrelic.TransportHTTP, - Body: unencodedBody, - Type: "HTTP", + Type: "micro", } txn.SetWebRequest(webReq) return txn } + +type wrappedServerStream struct { + stream server.Stream +} + +func (s wrappedServerStream) Context() context.Context { + return s.stream.Context() +} +func (s wrappedServerStream) Request() server.Request { + return s.stream.Request() +} +func (s wrappedServerStream) Send(msg any) error { + return s.stream.Send(msg) +} +func (s wrappedServerStream) Recv(msg any) error { + err := s.stream.Recv(msg) + if err != io.EOF { + messageType, version := getMessageType(msg) + newrelic.GetSecurityAgentInterface().SendEvent("GRPC", msg, messageType, version) + } + return err +} +func (s wrappedServerStream) Error() error { + return s.stream.Error() +} +func (s wrappedServerStream) Close() error { + return s.stream.Close() +} + +func getMessageType(req any) (string, string) { + messageType := "" + version := "v2" + messagev2, ok := req.(protoV2.Message) + if ok { + messageType = string(messagev2.ProtoReflect().Descriptor().FullName()) + } else { + messagev1, ok := req.(protoV1.Message) + if ok { + messageType = string(protoV1.MessageReflect(messagev1).Descriptor().FullName()) + version = "v1" + } + } + return messageType, version +} diff --git a/v3/integrations/nrsecurityagent/README.md b/v3/integrations/nrsecurityagent/README.md index ba77280c3..7ad9fb5b0 100644 --- a/v3/integrations/nrsecurityagent/README.md +++ b/v3/integrations/nrsecurityagent/README.md @@ -54,6 +54,8 @@ validator_service_url: wss://csec.nr-data.net detection: rxss: enabled: true +request: + body_limit:1 ``` * Based on additional packages imported by the user application, add suitable instrumentation package imports. diff --git a/v3/integrations/nrsecurityagent/go.mod b/v3/integrations/nrsecurityagent/go.mod index 4fc26a9ff..c0300761f 100644 --- a/v3/integrations/nrsecurityagent/go.mod +++ b/v3/integrations/nrsecurityagent/go.mod @@ -3,7 +3,7 @@ module github.com/newrelic/go-agent/v3/integrations/nrsecurityagent go 1.19 require ( - github.com/newrelic/csec-go-agent v0.4.0 + github.com/newrelic/csec-go-agent v0.5.1 github.com/newrelic/go-agent/v3 v3.26.0 github.com/newrelic/go-agent/v3/integrations/nrsqlite3 v1.2.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/v3/integrations/nrsecurityagent/nrsecurityagent.go b/v3/integrations/nrsecurityagent/nrsecurityagent.go index c7264d7ad..bb21f6765 100644 --- a/v3/integrations/nrsecurityagent/nrsecurityagent.go +++ b/v3/integrations/nrsecurityagent/nrsecurityagent.go @@ -30,6 +30,7 @@ func defaultSecurityConfig() SecurityConfig { cfg.Security.Mode = "IAST" cfg.Security.Agent.Enabled = true cfg.Security.Detection.Rxss.Enabled = true + cfg.Security.Request.BodyLimit = 300 return cfg } @@ -108,6 +109,8 @@ func ConfigSecurityFromYaml() ConfigOption { // NEW_RELIC_SECURITY_MODE scanning mode: "IAST" for now // NEW_RELIC_SECURITY_AGENT_ENABLED (boolean) // NEW_RELIC_SECURITY_DETECTION_RXSS_ENABLED (boolean) +// NEW_RELIC_SECURITY_REQUEST_BODY_LIMIT (integer) set limit on read request body in kb. By default, this is "300" + func ConfigSecurityFromEnvironment() ConfigOption { return func(cfg *SecurityConfig) { assignBool := func(field *bool, name string) { @@ -125,11 +128,22 @@ func ConfigSecurityFromEnvironment() ConfigOption { } } + assignInt := func(field *int, name string) { + if env := os.Getenv(name); env != "" { + if i, err := strconv.Atoi(env); nil != err { + cfg.Error = fmt.Errorf("invalid %s value: %s", name, env) + } else { + *field = i + } + } + } + assignBool(&cfg.Security.Enabled, "NEW_RELIC_SECURITY_ENABLED") assignString(&cfg.Security.Validator_service_url, "NEW_RELIC_SECURITY_VALIDATOR_SERVICE_URL") assignString(&cfg.Security.Mode, "NEW_RELIC_SECURITY_MODE") assignBool(&cfg.Security.Agent.Enabled, "NEW_RELIC_SECURITY_AGENT_ENABLED") assignBool(&cfg.Security.Detection.Rxss.Enabled, "NEW_RELIC_SECURITY_DETECTION_RXSS_ENABLED") + assignInt(&cfg.Security.Request.BodyLimit, "NEW_RELIC_SECURITY_REQUEST_BODY_LIMIT") } } @@ -160,3 +174,10 @@ func ConfigSecurityEnable(isEnabled bool) ConfigOption { cfg.Security.Enabled = isEnabled } } + +// ConfigSecurityRequestBodyLimit set limit on read request body in kb. By default, this is "300" +func ConfigSecurityRequestBodyLimit(bodyLimit int) ConfigOption { + return func(cfg *SecurityConfig) { + cfg.Security.Request.BodyLimit = bodyLimit + } +} diff --git a/v3/newrelic/context.go b/v3/newrelic/context.go index 731dcb73f..5ce186f3d 100644 --- a/v3/newrelic/context.go +++ b/v3/newrelic/context.go @@ -8,7 +8,6 @@ import ( "net/http" "github.com/newrelic/go-agent/v3/internal" - "github.com/valyala/fasthttp" ) // NewContext returns a new context.Context that carries the provided @@ -53,16 +52,3 @@ func transactionFromRequestContext(req *http.Request) *Transaction { } return txn } - -func transactionFromRequestContextFastHTTP(ctx *fasthttp.RequestCtx) *Transaction { - var txn *Transaction - if nil != ctx { - txn := ctx.UserValue("transaction").(*Transaction) - return txn - } - - if txn != nil { - return txn - } - return nil -} diff --git a/v3/newrelic/error_events.go b/v3/newrelic/error_events.go index 07aca4f35..6087865d8 100644 --- a/v3/newrelic/error_events.go +++ b/v3/newrelic/error_events.go @@ -32,7 +32,7 @@ func (e *errorEvent) WriteJSON(buf *bytes.Buffer) { w.stringField("spanId", e.SpanID) } if e.Expect { - w.stringField("error.expected", "true") + w.boolField(expectErrorAttr, true) } sharedTransactionIntrinsics(&e.txnEvent, &w) diff --git a/v3/newrelic/instrumentation.go b/v3/newrelic/instrumentation.go index e4351a955..d0ffd7379 100644 --- a/v3/newrelic/instrumentation.go +++ b/v3/newrelic/instrumentation.go @@ -5,31 +5,8 @@ package newrelic import ( "net/http" - - "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/fasthttpadaptor" ) -type fasthttpWrapperResponse struct { - ctx *fasthttp.RequestCtx -} - -func (rw fasthttpWrapperResponse) Header() http.Header { - hdrs := http.Header{} - rw.ctx.Request.Header.VisitAll(func(key, value []byte) { - hdrs.Add(string(key), string(value)) - }) - return hdrs -} - -func (rw fasthttpWrapperResponse) Write(b []byte) (int, error) { - return rw.ctx.Write(b) -} - -func (rw fasthttpWrapperResponse) WriteHeader(code int) { - rw.ctx.SetStatusCode(code) -} - // instrumentation.go contains helpers built on the lower level api. // WrapHandle instruments http.Handler handlers with Transactions. To @@ -99,54 +76,37 @@ func WrapHandle(app *Application, pattern string, handler http.Handler, options }) } -func WrapHandleFastHTTP(app *Application, pattern string, handler fasthttp.RequestHandler, options ...TraceOption) (string, fasthttp.RequestHandler) { - if app == nil { - return pattern, handler - } - - // add the wrapped function to the trace options as the source code reference point - // (but only if we know we're collecting CLM for this transaction and the user didn't already - // specify a different code location explicitly). - cache := NewCachedCodeLocation() +// AddCodeLevelMetricsTraceOptions adds trace options to an existing slice of TraceOption objects depending on how code level metrics is configured +// in your application. +// Please call cache:=newrelic.NewCachedCodeLocation() before calling this function, and pass the cache to us in order to allow you to optimize the +// performance and accuracy of this function. +func AddCodeLevelMetricsTraceOptions(app *Application, options []TraceOption, cache *CachedCodeLocation, cachedLocations ...interface{}) []TraceOption { + var tOptions *traceOptSet + var txnOptionList []TraceOption - return pattern, func(ctx *fasthttp.RequestCtx) { - var tOptions *traceOptSet - var txnOptionList []TraceOption + if cache == nil { + return options + } - if app.app != nil && app.app.run != nil && app.app.run.Config.CodeLevelMetrics.Enabled { - tOptions = resolveCLMTraceOptions(options) - if tOptions != nil && !tOptions.SuppressCLM && (tOptions.DemandCLM || app.app.run.Config.CodeLevelMetrics.Scope == 0 || (app.app.run.Config.CodeLevelMetrics.Scope&TransactionCLM) != 0) { - // we are for sure collecting CLM here, so go to the trouble of collecting this code location if nothing else has yet. - if tOptions.LocationOverride == nil { - if loc, err := cache.FunctionLocation(handler); err == nil { - WithCodeLocation(loc)(tOptions) - } + if app.app != nil && app.app.run != nil && app.app.run.Config.CodeLevelMetrics.Enabled { + tOptions = resolveCLMTraceOptions(options) + if tOptions != nil && !tOptions.SuppressCLM && (tOptions.DemandCLM || app.app.run.Config.CodeLevelMetrics.Scope == 0 || (app.app.run.Config.CodeLevelMetrics.Scope&TransactionCLM) != 0) { + // we are for sure collecting CLM here, so go to the trouble of collecting this code location if nothing else has yet. + if tOptions.LocationOverride == nil { + if loc, err := cache.FunctionLocation(cachedLocations); err == nil { + WithCodeLocation(loc)(tOptions) } } } - if tOptions == nil { - // we weren't able to curate the options above, so pass whatever we were given downstream - txnOptionList = options - } else { - txnOptionList = append(txnOptionList, withPreparedOptions(tOptions)) - } - - method := string(ctx.Method()) - path := string(ctx.Path()) - txn := app.StartTransaction(method+" "+path, txnOptionList...) - ctx.SetUserValue("transaction", txn) - defer txn.End() - r := &http.Request{} - fasthttpadaptor.ConvertRequest(ctx, r, true) - resp := fasthttpWrapperResponse{ctx: ctx} - - txn.SetWebResponse(resp) - txn.SetWebRequestHTTP(r) - - r = RequestWithTransactionContext(r, txn) - - handler(ctx) } + if tOptions == nil { + // we weren't able to curate the options above, so pass whatever we were given downstream + txnOptionList = options + } else { + txnOptionList = append(txnOptionList, withPreparedOptions(tOptions)) + } + + return txnOptionList } // WrapHandleFunc instruments handler functions using Transactions. To @@ -184,14 +144,6 @@ func WrapHandleFunc(app *Application, pattern string, handler func(http.Response return p, func(w http.ResponseWriter, r *http.Request) { h.ServeHTTP(w, r) } } -func WrapHandleFuncFastHTTP(app *Application, pattern string, handler func(*fasthttp.RequestCtx), options ...TraceOption) (string, func(*fasthttp.RequestCtx)) { - // add the wrapped function to the trace options as the source code reference point - // (to the beginning of the option list, so that the user can override this) - - p, h := WrapHandleFastHTTP(app, pattern, fasthttp.RequestHandler(handler), options...) - return p, func(ctx *fasthttp.RequestCtx) { h(ctx) } -} - // WrapListen wraps an HTTP endpoint reference passed to functions like http.ListenAndServe, // which causes security scanning to be done for that incoming endpoint when vulnerability // scanning is enabled. It returns the endpoint string, so you can replace a call like diff --git a/v3/newrelic/internal_17_test.go b/v3/newrelic/internal_17_test.go index 5ba7b6c7e..82d1dc8f1 100644 --- a/v3/newrelic/internal_17_test.go +++ b/v3/newrelic/internal_17_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/newrelic/go-agent/v3/internal" - "github.com/valyala/fasthttp" ) func myErrorHandler(w http.ResponseWriter, req *http.Request) { @@ -19,48 +18,6 @@ func myErrorHandler(w http.ResponseWriter, req *http.Request) { txn.NoticeError(myError{}) } -func myErrorHandlerFastHTTP(ctx *fasthttp.RequestCtx) { - ctx.WriteString("noticing an error") - txn := ctx.UserValue("transaction").(*Transaction) - txn.NoticeError(myError{}) -} - -func TestWrapHandleFastHTTPFunc(t *testing.T) { - app := testApp(nil, ConfigDistributedTracerEnabled(true), t) - - _, wrappedHandler := WrapHandleFuncFastHTTP(app.Application, "/hello", myErrorHandlerFastHTTP) - - if wrappedHandler == nil { - t.Error("Error when creating a wrapped handler") - } - ctx := &fasthttp.RequestCtx{} - ctx.Request.Header.SetMethod("GET") - ctx.Request.SetRequestURI("/hello") - wrappedHandler(ctx) - app.ExpectErrors(t, []internal.WantError{{ - TxnName: "WebTransaction/Go/GET /hello", - Msg: "my msg", - Klass: "newrelic.myError", - }}) - - app.ExpectMetrics(t, []internal.WantMetric{ - {Name: "WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: nil}, - {Name: "WebTransaction", Scope: "", Forced: true, Data: nil}, - {Name: "WebTransactionTotalTime/Go/GET /hello", Scope: "", Forced: false, Data: nil}, - {Name: "WebTransactionTotalTime", Scope: "", Forced: true, Data: nil}, - {Name: "HttpDispatcher", Scope: "", Forced: true, Data: nil}, - {Name: "Apdex", Scope: "", Forced: true, Data: nil}, - {Name: "Apdex/Go/GET /hello", Scope: "", Forced: false, Data: nil}, - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, - {Name: "Errors/all", Scope: "", Forced: true, Data: singleCount}, - {Name: "Errors/allWeb", Scope: "", Forced: true, Data: singleCount}, - {Name: "Errors/WebTransaction/Go/GET /hello", Scope: "", Forced: true, Data: singleCount}, - {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all", Scope: "", Forced: false, Data: nil}, - {Name: "ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb", Scope: "", Forced: false, Data: nil}, - }) -} - func TestWrapHandleFunc(t *testing.T) { app := testApp(nil, ConfigDistributedTracerEnabled(false), t) mux := http.NewServeMux() diff --git a/v3/newrelic/internal_context_test.go b/v3/newrelic/internal_context_test.go index 51372d382..1e15e61cd 100644 --- a/v3/newrelic/internal_context_test.go +++ b/v3/newrelic/internal_context_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/newrelic/go-agent/v3/internal" - "github.com/valyala/fasthttp" ) func TestWrapHandlerContext(t *testing.T) { @@ -37,30 +36,6 @@ func TestWrapHandlerContext(t *testing.T) { {Name: "Custom/mySegment", Scope: scope, Forced: false, Data: nil}, }) } -func TestExternalSegmentFastHTTP(t *testing.T) { - app := testApp(nil, ConfigDistributedTracerEnabled(false), t) - txn := app.StartTransaction("myTxn") - - req := fasthttp.AcquireRequest() - resp := fasthttp.AcquireResponse() - defer fasthttp.ReleaseRequest(req) - defer fasthttp.ReleaseResponse(resp) - - req.SetRequestURI("http://localhost:8080/hello") - req.Header.SetMethod("GET") - - ctx := &fasthttp.RequestCtx{} - seg := StartExternalSegmentFastHTTP(txn, ctx) - defer seg.End() - - txn.End() - app.ExpectMetrics(t, []internal.WantMetric{ - {Name: "OtherTransaction/Go/myTxn", Scope: "", Forced: true, Data: nil}, - {Name: "OtherTransaction/all", Scope: "", Forced: true, Data: nil}, - {Name: "OtherTransactionTotalTime/Go/myTxn", Scope: "", Forced: false, Data: nil}, - {Name: "OtherTransactionTotalTime", Scope: "", Forced: true, Data: nil}, - }) -} func TestStartExternalSegmentNilTransaction(t *testing.T) { // Test that StartExternalSegment pulls the transaction from the diff --git a/v3/newrelic/secure_agent.go b/v3/newrelic/secure_agent.go index 5011a3e7d..c7f546a80 100644 --- a/v3/newrelic/secure_agent.go +++ b/v3/newrelic/secure_agent.go @@ -4,7 +4,6 @@ import ( "net/http" ) -// // secureAgent is a global interface point for the nrsecureagent's hooks into the go agent. // The default value for this is a noOpSecurityAgent value, which has null definitions for // the methods. The Go compiler is expected to optimize away all the securityAgent method @@ -12,10 +11,8 @@ import ( // // If the nrsecureagent integration was initialized, it will register a real securityAgent // value in the securityAgent varialble instead, thus "activating" the hooks. -// var secureAgent securityAgent = noOpSecurityAgent{} -// // GetSecurityAgentInterface returns the securityAgent value // which provides the working interface to the installed // security agent (or to a no-op interface if none were @@ -26,7 +23,6 @@ var secureAgent securityAgent = noOpSecurityAgent{} // This avoids exposing the variable itself so it's not // writable externally and also sets up for the future if this // ends up not being a global variable later. -// func GetSecurityAgentInterface() securityAgent { return secureAgent } @@ -38,6 +34,7 @@ type securityAgent interface { IsSecurityActive() bool DistributedTraceHeaders(hdrs *http.Request, secureAgentevent any) SendExitEvent(any, error) + RequestBodyReadLimit() int } func (app *Application) RegisterSecurityAgent(s securityAgent) { @@ -88,13 +85,62 @@ func (t noOpSecurityAgent) DistributedTraceHeaders(hdrs *http.Request, secureAge func (t noOpSecurityAgent) SendExitEvent(secureAgentevent any, err error) { } +func (t noOpSecurityAgent) RequestBodyReadLimit() int { + return 300 * 1000 +} -// // IsSecurityAgentPresent returns true if there's an actual security agent hooked in to the // Go APM agent, whether or not it's enabled or operating in any particular mode. It returns // false only if the hook-in interface for those functions is a No-Op will null functionality. -// func IsSecurityAgentPresent() bool { _, isNoOp := secureAgent.(noOpSecurityAgent) return !isNoOp } + +type BodyBuffer struct { + buf []byte + isDataTruncated bool +} + +func (b *BodyBuffer) Write(p []byte) (int, error) { + if l := len(b.buf); len(p) <= secureAgent.RequestBodyReadLimit()-l { + b.buf = append(b.buf, p...) + return len(p), nil + } else if l := len(b.buf); secureAgent.RequestBodyReadLimit()-l > 1 { + end := secureAgent.RequestBodyReadLimit() - l + b.buf = append(b.buf, p[:end-1]...) + return end, nil + } else { + b.isDataTruncated = true + return 0, nil + } +} + +func (b *BodyBuffer) Len() int { + if b == nil { + return 0 + } + return len(b.buf) + +} + +func (b *BodyBuffer) read() []byte { + if b == nil { + return make([]byte, 0) + } + return b.buf +} + +func (b *BodyBuffer) isBodyTruncated() bool { + if b == nil { + return false + } + return b.isDataTruncated +} +func (b *BodyBuffer) String() (string, bool) { + if b == nil { + return "", false + } + return string(b.buf), b.isDataTruncated + +} diff --git a/v3/newrelic/segments.go b/v3/newrelic/segments.go index 65344033a..328db4283 100644 --- a/v3/newrelic/segments.go +++ b/v3/newrelic/segments.go @@ -5,9 +5,6 @@ package newrelic import ( "net/http" - - "github.com/valyala/fasthttp" - "github.com/valyala/fasthttp/fasthttpadaptor" ) // SegmentStartTime is created by Transaction.StartSegmentNow and marks the @@ -289,6 +286,23 @@ func (s *ExternalSegment) outboundHeaders() http.Header { return outboundHeaders(s) } +func (s *ExternalSegment) GetOutboundHeaders() http.Header { + return s.outboundHeaders() +} + +// SetSecureAgentEvent allows integration packages to set the secureAgentEvent +// for this external segment. That field is otherwise unexported and not available +// for other manipulation. +func (s *ExternalSegment) SetSecureAgentEvent(event any) { + s.secureAgentEvent = event +} + +// GetSecureAgentEvent retrieves the secureAgentEvent previously stored by +// a SetSecureAgentEvent method. +func (s *ExternalSegment) GetSecureAgentEvent() any { + return s.secureAgentEvent +} + // StartSegmentNow starts timing a segment. // // Deprecated: StartSegmentNow is deprecated and will be removed in a future @@ -340,36 +354,6 @@ func StartExternalSegment(txn *Transaction, request *http.Request) *ExternalSegm return s } -func StartExternalSegmentFastHTTP(txn *Transaction, ctx *fasthttp.RequestCtx) *ExternalSegment { - if nil == txn { - txn = transactionFromRequestContextFastHTTP(ctx) - } - request := &http.Request{} - - fasthttpadaptor.ConvertRequest(ctx, request, true) - s := &ExternalSegment{ - StartTime: txn.StartSegmentNow(), - Request: request, - } - if IsSecurityAgentPresent() { - s.secureAgentEvent = secureAgent.SendEvent("OUTBOUND", request) - } - - if request != nil && request.Header != nil { - for key, values := range s.outboundHeaders() { - for _, value := range values { - request.Header.Set(key, value) - } - } - - if IsSecurityAgentPresent() { - secureAgent.DistributedTraceHeaders(request, s.secureAgentEvent) - } - } - - return s -} - func addSpanAttr(start SegmentStartTime, key string, val interface{}) { if nil == start.thread { return diff --git a/v3/newrelic/transaction.go b/v3/newrelic/transaction.go index d1d519c63..af360f29a 100644 --- a/v3/newrelic/transaction.go +++ b/v3/newrelic/transaction.go @@ -4,7 +4,6 @@ package newrelic import ( - "bytes" "encoding/json" "fmt" "io" @@ -246,20 +245,14 @@ func serverName(r *http.Request) string { return "" } -func reqBody(req *http.Request) []byte { - var bodyBuffer bytes.Buffer - requestBuffer := make([]byte, 0) - bodyReader := io.TeeReader(req.Body, &bodyBuffer) - - if bodyReader != nil && req.Body != nil { - reqBuffer, err := io.ReadAll(bodyReader) - if err == nil { - requestBuffer = reqBuffer - } - r := io.NopCloser(bytes.NewBuffer(requestBuffer)) - req.Body = r +func reqBody(req *http.Request) *BodyBuffer { + if IsSecurityAgentPresent() { + buf := &BodyBuffer{buf: make([]byte, 0, 100)} + tee := io.TeeReader(req.Body, buf) + req.Body = io.NopCloser(tee) + return buf } - return bytes.TrimRight(requestBuffer, "\x00") + return nil } // SetWebRequest marks the transaction as a web transaction. SetWebRequest @@ -607,7 +600,7 @@ type WebRequest struct { // The following fields are needed for the secure agent's vulnerability // detection features. - Body []byte + Body *BodyBuffer ServerName string Type string RemoteAddress string @@ -634,7 +627,17 @@ func (webrequest WebRequest) GetHost() string { } func (webrequest WebRequest) GetBody() []byte { - return webrequest.Body + if webrequest.Body == nil { + return make([]byte, 0) + } + return webrequest.Body.read() +} + +func (webrequest WebRequest) IsDataTruncated() bool { + if webrequest.Body == nil { + return false + } + return webrequest.Body.isBodyTruncated() } func (webrequest WebRequest) GetServerName() string { diff --git a/v3/newrelic/version.go b/v3/newrelic/version.go index 0b8c0fc6f..085452c7a 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.27.0" + Version = "3.28.0" ) var ( From 77cfc282f62288e363598dde415d98bd6742db27 Mon Sep 17 00:00:00 2001 From: Steve Willoughby Date: Thu, 30 Nov 2023 11:58:50 -0800 Subject: [PATCH 3/3] updated go.mod files --- v3/integrations/nrgraphqlgo/go.mod | 2 ++ v3/integrations/nrgrpc/go.mod | 2 ++ v3/integrations/nrmicro/go.mod | 1 + v3/integrations/nrstan/test/go.mod | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/v3/integrations/nrgraphqlgo/go.mod b/v3/integrations/nrgraphqlgo/go.mod index 21f2e55db..f953df2e9 100644 --- a/v3/integrations/nrgraphqlgo/go.mod +++ b/v3/integrations/nrgraphqlgo/go.mod @@ -6,4 +6,6 @@ require ( github.com/graphql-go/graphql v0.8.1 github.com/newrelic/go-agent/v3 v3.28.1 ) + + replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrgrpc/go.mod b/v3/integrations/nrgrpc/go.mod index eea923f49..de50413b0 100644 --- a/v3/integrations/nrgrpc/go.mod +++ b/v3/integrations/nrgrpc/go.mod @@ -13,5 +13,7 @@ require ( google.golang.org/protobuf v1.30.0 ) + replace github.com/newrelic/go-agent/v3/integrations/nrsecurityagent => ../../integrations/nrsecurityagent + replace github.com/newrelic/go-agent/v3 => ../.. diff --git a/v3/integrations/nrmicro/go.mod b/v3/integrations/nrmicro/go.mod index 3b1d6002f..fda594e34 100644 --- a/v3/integrations/nrmicro/go.mod +++ b/v3/integrations/nrmicro/go.mod @@ -8,6 +8,7 @@ require ( github.com/golang/protobuf v1.5.3 github.com/micro/go-micro v1.8.0 github.com/newrelic/go-agent/v3 v3.28.1 + google.golang.org/protobuf v1.31.0 ) diff --git a/v3/integrations/nrstan/test/go.mod b/v3/integrations/nrstan/test/go.mod index 4c7be2421..6921303fe 100644 --- a/v3/integrations/nrstan/test/go.mod +++ b/v3/integrations/nrstan/test/go.mod @@ -5,7 +5,7 @@ module github.com/newrelic/go-agent/v3/integrations/nrstan/test go 1.19 require ( - github.com/nats-io/nats-streaming-server v0.25.5 + github.com/nats-io/nats-streaming-server v0.25.6 github.com/nats-io/stan.go v0.10.4 github.com/newrelic/go-agent/v3 v3.28.1 github.com/newrelic/go-agent/v3/integrations/nrstan v0.0.0