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
56 changes: 56 additions & 0 deletions v3/integrations/nrfasthttp/example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"fmt"
"os"
"time"

"github.com/newrelic/go-agent/v3/integrations/nrfasthttp"

newrelic "github.com/newrelic/go-agent/v3/newrelic"

"github.com/valyala/fasthttp"
)

func main() {
// Initialize New Relic
app, err := newrelic.NewApplication(
newrelic.ConfigAppName("httprouter 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)
}

// Define your handler
handler := nrfasthttp.NRHandler(app, func(ctx *fasthttp.RequestCtx) {
txn := nrfasthttp.GetTransaction(ctx)
client := &fasthttp.Client{}

req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetRequestURI("http://example.com")

res := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(res)

// Call nrfasthttp.Do instead of fasthttp.Do
err := nrfasthttp.Do(client, txn, req, res)

if err != nil {
fmt.Println("Request failed: ", err)
return
}
// Your handler logic here...
ctx.WriteString("Hello World")
})

// Start the server with the instrumented handler
fasthttp.ListenAndServe(":8080", handler)
}
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
)
21 changes: 21 additions & 0 deletions v3/integrations/nrfasthttp/nrfasthttp_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package nrfasthttp

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

type FastHTTPClient interface {
Do(req *fasthttp.Request, resp *fasthttp.Response) error
}

func Do(client FastHTTPClient, txn *newrelic.Transaction, req *fasthttp.Request, res *fasthttp.Response) error {
Copy link
Contributor

@iamemilio iamemilio Aug 29, 2023

Choose a reason for hiding this comment

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

We should provide wrappers for all the methods of the Client object if we're going to wrap it. Otherwise, customers will not have a way to wrap them. The question I have though is if its actually necessary to wrap it at all?

seg := txn.StartSegment("fasthttp-do")
err := client.Do(req, res)
if err != nil {
txn.NoticeError(err)
}
seg.End()

return err
}
75 changes: 75 additions & 0 deletions v3/integrations/nrfasthttp/nrfasthttp_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package nrfasthttp

import (
"net/http"

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

func init() { internal.TrackUsage("integration", "framework", "fasthttp") }

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 GetTransaction(ctx *fasthttp.RequestCtx) *newrelic.Transaction {
txn := ctx.UserValue("transaction")

if txn == nil {
return nil
}

return txn.(*newrelic.Transaction)
}

func NRHandler(app *newrelic.Application, original fasthttp.RequestHandler) fasthttp.RequestHandler {
iamemilio marked this conversation as resolved.
Show resolved Hide resolved
return func(ctx *fasthttp.RequestCtx) {
// Ignore reporting transaction for browser requesting .ico files
if string(ctx.Path()) == "/favicon.ico" {
original(ctx)
return
}

txn := app.StartTransaction(string(ctx.Path()))
defer txn.End()
ctx.SetUserValue("transaction", txn)

segRequest := txn.StartSegment("fasthttp-set-request")
// Set transaction attributes
txn.AddAttribute("method", string(ctx.Method()))
txn.AddAttribute("path", string(ctx.Path()))
// convert fasthttp request to http request
r := &http.Request{}
fasthttpadaptor.ConvertRequest(ctx, r, true)

txn.SetWebRequestHTTP(r)
txn.InsertDistributedTraceHeaders(r.Header)
iamemilio marked this conversation as resolved.
Show resolved Hide resolved
segRequest.End()

original(ctx)
// Set Web Response
seg := txn.StartSegment("fasthttp-set-response")
resp := fasthttpWrapperResponse{ctx: ctx}
txn.SetWebResponse(resp)
seg.End()
}
}
101 changes: 101 additions & 0 deletions v3/integrations/nrfasthttp/nrfasthttp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package nrfasthttp

import (
"github.com/newrelic/go-agent/v3/internal"
"github.com/newrelic/go-agent/v3/internal/integrationsupport"
newrelic "github.com/newrelic/go-agent/v3/newrelic"

"errors"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
)

type mockFastHTTPClient struct {
err error
}

func (m *mockFastHTTPClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error {

if m.err != nil {
return m.err
}
return nil
}

func TestFastHTTPWrapperResponseHeader(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.Set("X-Test-Header", "test")
wrapper := fasthttpWrapperResponse{ctx: ctx}
hdrs := wrapper.Header()
assert.Equal(t, "test", hdrs.Get("X-Test-Header"))
}

func TestFastHTTPWrapperResponseWrite(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
wrapper := fasthttpWrapperResponse{ctx: ctx}

w, err := wrapper.Write([]byte("Hello World!"))
assert.Nil(t, err)
assert.Equal(t, 12, w)
assert.Equal(t, "Hello World!", string(ctx.Response.Body()))
}

func TestFastHTTPWrapperResponseWriteHeader(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
wrapper := fasthttpWrapperResponse{ctx: ctx}
wrapper.WriteHeader(http.StatusForbidden)
assert.Equal(t, http.StatusForbidden, ctx.Response.StatusCode())
}

func TestGetTransaction(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
txn := &newrelic.Transaction{}
ctx.SetUserValue("transaction", txn)
assert.Equal(t, txn, GetTransaction(ctx))
}

func TestNRHandler(t *testing.T) {
app := integrationsupport.NewBasicTestApp()
original := func(ctx *fasthttp.RequestCtx) {
ctx.WriteString("Hello World!")
}
nrhandler := NRHandler(app.Application, original)
ctx := &fasthttp.RequestCtx{}
nrhandler(ctx)
assert.Equal(t, "Hello World!", string(ctx.Response.Body()))
app.ExpectMetrics(t, []internal.WantMetric{
{Name: "WebTransaction"},
{Name: "Apdex/Go/"},
{Name: "Custom/fasthttp-set-response"},
{Name: "WebTransactionTotalTime/Go/"},
{Name: "Custom/fasthttp-set-request"},
{Name: "WebTransaction/Go/"},
{Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"},
{Name: "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb"},
{Name: "WebTransactionTotalTime"},
{Name: "Apdex"},
{Name: "Custom/fasthttp-set-request", Scope: "WebTransaction/Go/", Forced: false, Data: nil},
{Name: "Custom/fasthttp-set-response", Scope: "WebTransaction/Go/", Forced: false, Data: nil},
{Name: "HttpDispatcher"},
})
}

func TestDo(t *testing.T) {
client := &mockFastHTTPClient{}
txn := &newrelic.Transaction{}
req := &fasthttp.Request{}
resp := &fasthttp.Response{}

// check for no error
err := Do(client, txn, req, resp)
assert.NoError(t, err)

// check for error
client.err = errors.New("ahh!!")
err = Do(client, txn, req, resp)

assert.Error(t, err)
}
Loading