Skip to content

Commit

Permalink
FastHTTP Integration (#774)
Browse files Browse the repository at this point in the history
Added Support For FastHTTP
  • Loading branch information
mirackara authored Sep 14, 2023
1 parent 0afb8ac commit 5b2310b
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 5 deletions.
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()

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{}
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{
{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

0 comments on commit 5b2310b

Please sign in to comment.