Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(WIP) FastHTTP Integration #774

Merged
merged 14 commits into from
Sep 14, 2023
62 changes: 62 additions & 0 deletions v3/examples/client-fasthttp/main.go
Original file line number Diff line number Diff line change
@@ -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()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctx doesn't contain any outbound req attributes. it's just an empty instance of RequestCtx.
We would require outbound req URI and header for generating trace events.
@mirackara

Copy link
Contributor Author

@mirackara mirackara Oct 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aayush-ap Is this still the case if this is on the client side? In the regular client example we do the same thing as well.

Copy link
Contributor

@aayush-ap aayush-ap Oct 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mirackara In the regular client example we are passing HTTP request object as an attribute of StartExternalSegment which contain outbound req parameters like URL and all.

but in the case of fastHttp you are passing fast HTTP RequestCtx object as an attribute of StartExternalSegmentFastHTTP
ctx doesn't contain any outbound req attributes. it's just an empty instance of RequestCtx.

example:

req := fasthttp.AcquireRequest()
  .....
  ......
 req.SetRequestURI("https://www.google.com/")
 ctx := &fasthttp.RequestCtx{}
 seg := newrelic.StartExternalSegmentFastHTTP(txn, ctx)
 defer seg.End()

 err := fasthttp.Do(req, resp)

  • Expected :
    {
    "name":"External/www.google.com/https",
    "scope":"WebTransaction/Go/GET /case1"
    }

  • Actual :
    {
    "name":"External/unknown/http/GET",
    "scope":"WebTransaction/Go/GET /doRequest"
    },

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the following fix for outbound request

k2io@15a72c8

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)
}
58 changes: 58 additions & 0 deletions v3/examples/server-fasthttp/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions v3/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
9 changes: 9 additions & 0 deletions v3/integrations/nrfasthttp/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
14 changes: 14 additions & 0 deletions v3/newrelic/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
91 changes: 86 additions & 5 deletions v3/newrelic/instrumentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Copy link
Contributor

@aayush-ap aayush-ap Oct 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not able to access inbound request response with fasthttpWrapperResponse wrapper
@mirackara

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:
Expand Down Expand Up @@ -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:
//
Expand Down Expand Up @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions v3/newrelic/internal_17_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the wrapHandle test also creates and expects an error. Can we do that for the tests for fasthttp as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's an example of noticing an error in the fasthttp-server example :) I don't think it should be too difficult to add that into the internal testing as well.

{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()
Expand Down
29 changes: 29 additions & 0 deletions v3/newrelic/internal_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/newrelic/go-agent/v3/internal"
"github.com/valyala/fasthttp"
)

func TestWrapHandlerContext(t *testing.T) {
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading