From 5b2310b0cb056a21e09ab5c6c20b42637f3ef155 Mon Sep 17 00:00:00 2001 From: Mirac Kara <55501260+mirackara@users.noreply.github.com> Date: Thu, 14 Sep 2023 13:06:29 -0500 Subject: [PATCH] FastHTTP Integration (#774) Added Support For FastHTTP --- v3/examples/client-fasthttp/main.go | 62 +++++++++++++++++++ v3/examples/server-fasthttp/main.go | 58 ++++++++++++++++++ v3/go.mod | 1 + v3/integrations/nrfasthttp/go.mod | 9 +++ v3/newrelic/context.go | 14 +++++ v3/newrelic/instrumentation.go | 91 ++++++++++++++++++++++++++-- v3/newrelic/internal_17_test.go | 43 +++++++++++++ v3/newrelic/internal_context_test.go | 29 +++++++++ v3/newrelic/segments.go | 33 ++++++++++ 9 files changed, 335 insertions(+), 5 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 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/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..64b73c08f 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,34 @@ 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() + + err := fasthttp.Do(req, resp) + 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}, + }) + if err != nil { + t.Error(err) + } +} 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