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

feat: instrumentation of fasthttp client methods #974

Merged
merged 21 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example/basic_usage_fasthttp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ The available routes are,
- [localhost:8080/round-trip](http://localhost:7070/round-trip)
- [localhost:8080/error-handler](http://localhost:7070/error-handler)
- [localhost:8080/panic-handler](http://localhost:7070/panic-handler)
- [localhost:8080/client-call-handler](http://localhost:7070/client-call-handler)

After issuing a couple of API requests, you will be able to see the call traces in the Instana dashboard.
2 changes: 1 addition & 1 deletion example/basic_usage_fasthttp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.22

require (
github.com/instana/go-sensor v1.66.0
github.com/instana/go-sensor/instrumentation/instafasthttp v0.0.0-20241021051914-d1fd3525c5b5
github.com/instana/go-sensor/instrumentation/instafasthttp v0.1.0
github.com/instana/go-sensor/instrumentation/instagorm v1.15.0
github.com/valyala/fasthttp v1.58.0
gorm.io/driver/postgres v1.5.11
Expand Down
54 changes: 54 additions & 0 deletions example/basic_usage_fasthttp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log"
"time"

instana "github.com/instana/go-sensor"
"github.com/instana/go-sensor/instrumentation/instafasthttp"
Expand Down Expand Up @@ -68,6 +69,8 @@ func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
instafasthttp.TraceHandler(sensor, "panic-handler", "/panic-handler", panicHandler)(ctx)
case "/round-trip":
instafasthttp.TraceHandler(sensor, "round-trip", "/round-trip", roundTripHandler)(ctx)
case "/client-call-handler":
instafasthttp.TraceHandler(sensor, "client-call-handler", "/client-call-handler", clientCallHandler)(ctx)
default:
ctx.Error("Unsupported path", fasthttp.StatusNotFound)
}
Expand Down Expand Up @@ -122,6 +125,57 @@ func roundTripHandler(ctx *fasthttp.RequestCtx) {

}

func clientCallHandler(ctx *fasthttp.RequestCtx) {
uCtx := instafasthttp.UserContext(ctx)

url := fasthttp.AcquireURI()
url.Parse(nil, []byte("http://localhost:7070/greet"))

// You may read the timeouts from some config
readTimeout, _ := time.ParseDuration("500ms")
writeTimeout, _ := time.ParseDuration("500ms")
maxIdleConnDuration, _ := time.ParseDuration("1h")
c := &fasthttp.Client{
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
MaxIdleConnDuration: maxIdleConnDuration,
NoDefaultUserAgentHeader: true, // Don't send: User-Agent: fasthttp
DisableHeaderNamesNormalizing: true, // If you set the case on your headers correctly you can enable this
DisablePathNormalizing: true,
// increase DNS cache time to an hour instead of default minute
Dial: (&fasthttp.TCPDialer{
Concurrency: 4096,
DNSCacheDuration: time.Hour,
}).Dial,
}

// create instana instrumented client
ic := instafasthttp.GetInstrumentedClient(sensor, c)

req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetURI(url)
fasthttp.ReleaseURI(url) // now you may release the URI
req.Header.SetMethod(fasthttp.MethodGet)

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

// Make the request
err := ic.Do(uCtx, req, resp)
if err != nil {
log.Fatalf("failed to GET http://localhost:7070/greet: %s", err)
}

bs := string(resp.Body())

fmt.Println(bs)

ctx.SetStatusCode(fasthttp.StatusOK)
fmt.Fprintf(ctx, bs)

}

func panicHandler(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "This is a panic!\n")
panic(errors.New("This is a panic!"))
Expand Down
55 changes: 53 additions & 2 deletions instrumentation/instafasthttp/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Instana instrumentation for fasthttp
instafasthttp - Instana instrumentation for fasthttp
=====================================

This package provides Instana instrumentation for the [`fasthttp`](https://pkg.go.dev/github.com/valyala/fasthttp) package.
Expand Down Expand Up @@ -67,7 +67,7 @@ func greetEndpointHandler(ctx *fasthttp.RequestCtx) {
}
```

### RoundTripper
### HostClient

The `instafasthttp.RoundTripper` provides an implementation of the `fasthttp.RoundTripper` interface. It can be used to instrument client calls with the help of `instafasthttp.HostClient`. Refer to the details below for more information.

Expand Down Expand Up @@ -125,3 +125,54 @@ func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
log.Fatal(fasthttp.ListenAndServe(":7070", fastHTTPHandler))

```
### Client

The `client.Do` and related methods can be traced using Instana. However, the usage differs slightly from that of the standard HostClient. Below are the steps to use an Instana instrumented client.

- To enable tracing, you must create an instrumented client using the `instafasthttp.GetInstrumentedClient` method as shown below:

```go
// fasthttp client
client := &fasthttp.Client{
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
MaxIdleConnDuration: maxIdleConnDuration,
NoDefaultUserAgentHeader: true, // Don't send: User-Agent: fasthttp
DisableHeaderNamesNormalizing: true, // If you set the case on your headers correctly you can enable this
DisablePathNormalizing: true,
// increase DNS cache time to an hour instead of default minute
Dial: (&fasthttp.TCPDialer{
Concurrency: 4096,
DNSCacheDuration: time.Hour,
}).Dial,
}

// create instana instrumented client
ic := instafasthttp.GetInstrumentedClient(sensor, client)
```
- Use the instrumented client(ic) for all requests instead of the original client.
- Tracing is supported for the following methods, where an additional `context.Context` parameter is required as the first argument. Ensure the context is set properly for span correlation:
1. Do
2. DoTimeout
3. DoDeadline
4. DoRedirects

```go
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetURI(url)
fasthttp.ReleaseURI(url) // now you may release the URI
req.Header.SetMethod(fasthttp.MethodGet)

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

// Make the request
err := ic.Do(uCtx, req, resp)
if err != nil {
log.Fatalf("failed to GET http://localhost:7070/greet: %s", err)
}
```

- For methods other than the four listed above, use the usual method signatures without passing a context. These methods will not support tracing.
- Use the `Unwrap()` method if you require the original fasthttp.Client instance. However, avoid using the unwrapped instance directly for the above four methods, as Instana tracing will not be applied in such cases.
231 changes: 231 additions & 0 deletions instrumentation/instafasthttp/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// (c) Copyright IBM Corp. 2024

package instafasthttp

import (
"context"
"time"

instana "github.com/instana/go-sensor"
"github.com/valyala/fasthttp"
)

// GetInstrumentedClient returns an instrumented instafasthttp.Client instance derived from a *fasthttp.Client instance
func GetInstrumentedClient(sensor instana.TracerLogger, orgClient *fasthttp.Client) Client {
return &instaClient{
Client: orgClient,
sensor: sensor,
}
}

// Instrumented fasthttp.Client
//
// Most of the methods are the same as those in fasthttp.Client.
//
// Only Do, DoTimeout, and DoDeadline differ from fasthttp.Client.
// Please use these methods instead of their fasthttp.Client counterparts to enable tracing.
type Client interface {
// The following methods are from the original *fasthttp.Client; there is no need to implement them.

// Get returns the status code and body of url.
//
// The contents of dst will be replaced by the body and returned, if the dst
// is too small a new slice will be allocated.
//
// The function follows redirects. Use Do* for manually handling redirects.
Get(dst []byte, url string) (statusCode int, body []byte, err error)

// GetTimeout returns the status code and body of url.
//
// The contents of dst will be replaced by the body and returned, if the dst
// is too small a new slice will be allocated.
//
// The function follows redirects. Use Do* for manually handling redirects.
//
// ErrTimeout error is returned if url contents couldn't be fetched
// during the given timeout.
GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error)

// GetDeadline returns the status code and body of url.
//
// The contents of dst will be replaced by the body and returned, if the dst
// is too small a new slice will be allocated.
//
// The function follows redirects. Use Do* for manually handling redirects.
//
// ErrTimeout error is returned if url contents couldn't be fetched
// until the given deadline.
GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error)

// Post sends POST request to the given url with the given POST arguments.
//
// The contents of dst will be replaced by the body and returned, if the dst
// is too small a new slice will be allocated.
//
// The function follows redirects. Use Do* for manually handling redirects.
//
// Empty POST body is sent if postArgs is nil.
Post(dst []byte, url string, postArgs *fasthttp.Args) (statusCode int, body []byte, err error)

// CloseIdleConnections closes any connections which were previously
// connected from previous requests but are now sitting idle in a
// "keep-alive" state. It does not interrupt any connections currently
// in use.
CloseIdleConnections()

// DoTimeout performs the given request and waits for response during
// the given timeout duration.
//
// Request must contain at least non-zero RequestURI with full url (including
// scheme and host) or non-zero Host header + RequestURI.
//
// Client determines the server to be requested in the following order:
//
// - from RequestURI if it contains full url with scheme and host;
// - from Host header otherwise.
//
// The function doesn't follow redirects. Use Get* for following redirects.
//
// Response is ignored if resp is nil.
//
// ErrTimeout is returned if the response wasn't returned during
// the given timeout.
// Immediately returns ErrTimeout if timeout value is negative.
//
// ErrNoFreeConns is returned if all Client.MaxConnsPerHost connections
// to the requested host are busy.
//
// It is recommended obtaining req and resp via Acquire
//
// Pass a valid context as the first argument for for span correlation
DoTimeout(ctx context.Context, req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error

// DoDeadline performs the given request and waits for response until
// the given deadline.
//
// Request must contain at least non-zero RequestURI with full url (including
// scheme and host) or non-zero Host header + RequestURI.
//
// Client determines the server to be requested in the following order:
//
// - from RequestURI if it contains full url with scheme and host;
// - from Host header otherwise.
//
// The function doesn't follow redirects. Use Get* for following redirects.
//
// Response is ignored if resp is nil.
//
// ErrTimeout is returned if the response wasn't returned until
// the given deadline.
// Immediately returns ErrTimeout if the deadline has already been reached.
//
// ErrNoFreeConns is returned if all Client.MaxConnsPerHost connections
// to the requested host are busy.
//
// It is recommended obtaining req and resp via AcquireRequest
// and AcquireResponse in performance-critical code.
//
// Pass a valid context as the first argument for for span correlation
DoDeadline(ctx context.Context, req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error

// DoRedirects performs the given http request and fills the given http response,
// following up to maxRedirectsCount redirects. When the redirect count exceeds
// maxRedirectsCount, ErrTooManyRedirects is returned.
//
// Request must contain at least non-zero RequestURI with full url (including
// scheme and host) or non-zero Host header + RequestURI.
//
// Client determines the server to be requested in the following order:
//
// - from RequestURI if it contains full url with scheme and host;
// - from Host header otherwise.
//
// Response is ignored if resp is nil.
//
// ErrNoFreeConns is returned if all DefaultMaxConnsPerHost connections
// to the requested host are busy.
//
// It is recommended obtaining req and resp via AcquireRequest
// and AcquireResponse in performance-critical code.
//
// Pass a valid context as the first argument for for span correlation
DoRedirects(ctx context.Context, req *fasthttp.Request, resp *fasthttp.Response, maxRedirectsCount int) error

// Do performs the given http request and fills the given http response.
//
// Request must contain at least non-zero RequestURI with full url (including
// scheme and host) or non-zero Host header + RequestURI.
//
// Client determines the server to be requested in the following order:
//
// - from RequestURI if it contains full url with scheme and host;
// - from Host header otherwise.
//
// Response is ignored if resp is nil.
//
// The function doesn't follow redirects. Use Get* for following redirects.
//
// ErrNoFreeConns is returned if all Client.MaxConnsPerHost connections
// to the requested host are busy.
//
// It is recommended obtaining req and resp via AcquireRequest
// and AcquireResponse in performance-critical code.
//
// Pass a valid context as the first argument for for span correlation
Do(ctx context.Context, req *fasthttp.Request, resp *fasthttp.Response) error

// Unwrap returns the original *fasthttp.Client
Unwrap() *fasthttp.Client
}

type instaClient struct {
*fasthttp.Client
sensor instana.TracerLogger
}

func (ic *instaClient) Unwrap() *fasthttp.Client {
return ic.Client
}

func (ic *instaClient) DoTimeout(ctx context.Context, req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error {
cfp := &clientFuncParams{
sensor: ic.sensor,
ic: ic,
clientFuncType: doWithTimeoutFunc,
timeout: timeout,
}
_, err := instrumentClient(ctx, req, resp, cfp)
return err
}

func (ic *instaClient) DoDeadline(ctx context.Context, req *fasthttp.Request, resp *fasthttp.Response, deadline time.Time) error {
cfp := &clientFuncParams{
sensor: ic.sensor,
ic: ic,
clientFuncType: doWithDeadlineFunc,
deadline: deadline,
}
_, err := instrumentClient(ctx, req, resp, cfp)
return err
}

func (ic *instaClient) DoRedirects(ctx context.Context, req *fasthttp.Request, resp *fasthttp.Response, maxRedirectsCount int) error {
cfp := &clientFuncParams{
sensor: ic.sensor,
ic: ic,
clientFuncType: doWithRedirectsFunc,
maxRedirectsCount: maxRedirectsCount,
}
_, err := instrumentClient(ctx, req, resp, cfp)
return err
}

func (ic *instaClient) Do(ctx context.Context, req *fasthttp.Request, resp *fasthttp.Response) error {
cfp := &clientFuncParams{
sensor: ic.sensor,
ic: ic,
clientFuncType: doFunc,
}
_, err := instrumentClient(ctx, req, resp, cfp)
return err
}
Loading
Loading