-
Notifications
You must be signed in to change notification settings - Fork 295
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
Changes from all commits
f999c57
79146fb
a36c213
dd7d252
fc0b173
3108963
55af84a
349fe30
7f56709
de5ca15
f1750de
ef22a97
aed3dde
f2aea48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} |
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) | ||
} |
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 | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not able to access inbound request response with |
||
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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
Expected :
{
"name":"External/www.google.com/https",
"scope":"WebTransaction/Go/GET /case1"
}
Actual :
{
"name":"External/unknown/http/GET",
"scope":"WebTransaction/Go/GET /doRequest"
},
There was a problem hiding this comment.
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