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

[SLT-331] feat(omnirpc): omnirpc request tracing #3289

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
72 changes: 68 additions & 4 deletions services/omnirpc/http/capture_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,26 @@ package http
import (
"context"

"github.com/ethereum/go-ethereum/common"
"github.com/synapsecns/sanguine/core/bytemap"
"github.com/synapsecns/sanguine/core/metrics"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)

// CaptureClient is a mock client used for checking response values.
type CaptureClient struct {
requests []*CapturedRequest
responseFunc MakeResponseFunc
handler metrics.Handler
}

// MakeResponseFunc is used for mocking responses.
type MakeResponseFunc func(c *CapturedRequest) (Response, error)

// NewCaptureClient creates anew client for testing.
func NewCaptureClient(responseFunc MakeResponseFunc) *CaptureClient {
return &CaptureClient{requests: []*CapturedRequest{}, responseFunc: responseFunc}
func NewCaptureClient(handler metrics.Handler, responseFunc MakeResponseFunc) *CaptureClient {
return &CaptureClient{requests: []*CapturedRequest{}, responseFunc: responseFunc, handler: handler}
}

// Requests turns a list of sent requests. These are not mutation safe.
Expand All @@ -30,6 +35,7 @@ func (c *CaptureClient) NewRequest() Request {
request := CapturedRequest{
Client: c,
StringHeaders: make(map[string]string),
Handler: c.handler,
}
c.requests = append(c.requests, &request)
return &request
Expand All @@ -56,44 +62,102 @@ type CapturedRequest struct {
// RequestURIBytes is the request uri bytes. Notably, this will not include
// RequestURI's set by SetRequestURI
RequestURIBytes []byte
// Metrics is the metrics handler
Handler metrics.Handler
Comment on lines +66 to +67
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enforce the association of metrics.Handler with CapturedRequest.

To prevent issues with uninitialized c.Handler, consider requiring the metrics.Handler to be set during the creation of CapturedRequest instances.

Modify the NewRequest method to accept a metrics.Handler parameter:

// NewRequest creates a new request.
-func (c *CaptureClient) NewRequest() Request {
+func (c *CaptureClient) NewRequest(handler metrics.Handler) Request {
	request := CapturedRequest{
		Client:        c,
		StringHeaders: make(map[string]string),
+		Handler:       handler,
+		Context:       context.Background(),
	}
	c.requests = append(c.requests, &request)
	return &request
}

This ensures that every CapturedRequest has a valid Handler and Context from the beginning.

Also applies to: 161-164

}

var _ Client = &CaptureClient{}

// SetBody stores the body for testing.
func (c *CapturedRequest) SetBody(body []byte) Request {
_, span := c.Handler.Tracer().Start(
Copy link
Contributor

Choose a reason for hiding this comment

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

i wouldn't do this here. I'd have a single trace around when Do() si called and set the attributes there (http.body) specifically

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed

c.Context,
"SetBody",
trace.WithAttributes(attribute.String("SetBody", common.Bytes2Hex(body))),
)
defer func() {
metrics.EndSpan(span)
}()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle potential nil references for c.Handler.

In the methods SetBody, SetHeader, SetHeaderBytes, SetRequestURI, and Do, there is a possibility of a nil pointer dereference if c.Handler is not set before invoking c.Handler.Tracer(). Ensure that c.Handler is properly initialized before these methods are called, or add nil checks to prevent runtime panics.

Consider adding a nil check at the beginning of each method:

+	if c.Handler == nil {
+		// Handle nil Handler appropriately, e.g., initialize it or return an error.
+		return c
+	}

Alternatively, ensure that c.Handler is always set by mandating the use of WithMetrics before these methods are called.

Also applies to: 99-107, 114-122, 129-136, 143-159


⚠️ Potential issue

Ensure c.Context is initialized before usage in tracing.

In methods like SetBody, SetHeader, SetHeaderBytes, SetRequestURI, and Do, c.Context is used to start tracing spans. If SetContext has not been called prior to these methods, c.Context may be nil, leading to unexpected behavior.

Initialize c.Context with a default context in the CapturedRequest struct:

// During CapturedRequest initialization
CapturedRequest{
	Client:        c,
	StringHeaders: make(map[string]string),
+	Context:       context.Background(),
}

Alternatively, check for nil and initialize within each method:

+	if c.Context == nil {
+		c.Context = context.Background()
+	}

Also applies to: 99-107, 114-122, 129-136, 143-159

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential nil dereference of Handler and Context in SetBody

In the SetBody method, c.Handler and c.Context are used without checking if they are initialized. If either c.Handler or c.Context is nil, this could lead to a panic at runtime.

Consider initializing c.Handler and c.Context with default values when creating a CapturedRequest, or add checks to ensure they are not nil before use.

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure 'Handler' is not nil before usage to prevent nil pointer dereference

In the methods SetBody, SetContext, SetHeader, SetHeaderBytes, SetRequestURI, and Do, the code calls c.Handler.Tracer() without checking if Handler is initialized. If Handler is nil, this will cause a nil pointer dereference at runtime.

Consider initializing Handler in NewRequest() or adding nil checks before using it. For example, initialize Handler with a no-op metrics handler:

 func (c *CaptureClient) NewRequest() Request {
 	request := CapturedRequest{
 		Client:        c,
 		StringHeaders: make(map[string]string),
+		Handler:       metrics.NoopHandler(),
 	}
 	c.requests = append(c.requests, &request)
 	return &request
 }

Alternatively, add nil checks before using Handler:

if c.Handler != nil {
	// Proceed to start the span
} else {
	// Handle the case where Handler is nil, possibly by initializing it or skipping tracing
}

Also applies to: 85-92, 99-107, 114-122, 129-136, 143-149

c.Body = body
return c
}

// SetContext stores the context for testing.
func (c *CapturedRequest) SetContext(ctx context.Context) Request {
_, span := c.Handler.Tracer().Start(
ctx,
"SetContext",
)
span.AddEvent("SetContext")
defer func() {
metrics.EndSpan(span)
}()
c.Context = ctx
return c
}

// SetHeader sets the header for testing.
func (c *CapturedRequest) SetHeader(key, value string) Request {
_, span := c.Handler.Tracer().Start(
Copy link
Contributor

Choose a reason for hiding this comment

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

see above

c.Context,
"SetHeader",
trace.WithAttributes(attribute.String("SetHeader", key)),
trace.WithAttributes(attribute.String("value", value)),
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Combine multiple attributes into a single WithAttributes call.

In the SetHeader method, multiple trace.WithAttributes calls are used separately. Only the last WithAttributes call will take effect due to the variadic nature of the function parameters.

Combine all attributes into a single WithAttributes call to ensure all attributes are recorded:

_, span := c.Handler.Tracer().Start(
	c.Context,
	"SetHeader",
-	trace.WithAttributes(attribute.String("SetHeader", key)),
-	trace.WithAttributes(attribute.String("value", value)),
+	trace.WithAttributes(
+		attribute.String("SetHeader", key),
+		attribute.String("value", value),
+	),
)

This applies to similar cases in other methods as well.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
c.Context,
"SetHeader",
trace.WithAttributes(attribute.String("SetHeader", key)),
trace.WithAttributes(attribute.String("value", value)),
)
c.Context,
"SetHeader",
trace.WithAttributes(
attribute.String("SetHeader", key),
attribute.String("value", value),
),

defer func() {
metrics.EndSpan(span)
}()
c.StringHeaders[key] = value
return c
}

// SetHeaderBytes sets header bytes for testing.
func (c *CapturedRequest) SetHeaderBytes(key, value []byte) Request {
_, span := c.Handler.Tracer().Start(
c.Context,
"SetHeaderBytes",
trace.WithAttributes(attribute.String("key", common.Bytes2Hex(key))),
trace.WithAttributes(attribute.String("value", common.Bytes2Hex(value))),
)
defer func() {
metrics.EndSpan(span)
}()
c.ByteHeaders.Put(key, value)
return c
}

// SetRequestURI stores the request uri.
func (c *CapturedRequest) SetRequestURI(uri string) Request {
_, span := c.Handler.Tracer().Start(
c.Context,
"SetRequestURI",
trace.WithAttributes(attribute.String("uri", uri)),
)
defer func() {
metrics.EndSpan(span)
}()
c.RequestURI = uri
return c
}

// Do calls responseFunc for testing.
func (c *CapturedRequest) Do() (Response, error) {
//nolint: wrapcheck
return c.Client.responseFunc(c)
_, span := c.Handler.Tracer().Start(
c.Context,
"Do",
)
defer func() {
metrics.EndSpan(span)
}()

resp, err := c.Client.responseFunc(c)
if err != nil {
return nil, err
}

span.SetAttributes(attribute.String("response", common.Bytes2Hex(resp.Body())))

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle potential nil response body in 'Do' method

In the Do method, after calling responseFunc, you set an attribute with resp.Body(). If resp is nil or resp.Body() returns nil, this could cause a panic. Consider checking if resp and resp.Body() are not nil before setting the attribute.

Apply this diff:

-	span.SetAttributes(attribute.String("response", common.Bytes2Hex(resp.Body())))
+	if resp != nil && resp.Body() != nil {
+		span.SetAttributes(attribute.String("response", common.Bytes2Hex(resp.Body())))
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
span.SetAttributes(attribute.String("response", common.Bytes2Hex(resp.Body())))
if resp != nil && resp.Body() != nil {
span.SetAttributes(attribute.String("response", common.Bytes2Hex(resp.Body())))
}

return resp, err
}

var _ Request = &CapturedRequest{}
18 changes: 11 additions & 7 deletions services/omnirpc/http/capture_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@ package http_test

import (
"context"
"testing"

"github.com/brianvoe/gofakeit/v6"
. "github.com/stretchr/testify/assert"
"github.com/synapsecns/sanguine/core/metrics"
"github.com/synapsecns/sanguine/services/omnirpc/http"
"github.com/synapsecns/sanguine/services/omnirpc/http/mocks"
"testing"
)

func TestCaptureClient(t *testing.T) {
testRes := gofakeit.ImageJpeg(50, 50)
testBody := gofakeit.ImageJpeg(50, 50)

client := http.NewCaptureClient(func(c *http.CapturedRequest) (http.Response, error) {
bodyRes := new(mocks.Response)
bodyRes.On("Body").Return(testRes)
client := http.NewCaptureClient(
metrics.NewNullHandler(),
func(c *http.CapturedRequest) (http.Response, error) {
bodyRes := new(mocks.Response)
bodyRes.On("Body").Return(testRes)

return bodyRes, nil
})
return bodyRes, nil
})

testCtx := context.Background()

Expand All @@ -31,8 +35,8 @@ func TestCaptureClient(t *testing.T) {
testURL := gofakeit.URL()

testReq := client.NewRequest()
testReq.SetBody(testBody)
testReq.SetContext(testCtx)
testReq.SetBody(testBody)
testReq.SetHeaderBytes(byteHeaderK, byteHeaderV)
testReq.SetHeader(strHeaderK, strHeaderV)
testReq.SetRequestURI(testURL)
Expand Down
10 changes: 6 additions & 4 deletions services/omnirpc/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package http
import (
"context"
"strings"

"github.com/synapsecns/sanguine/core/metrics"
)

// Client contains a post client for interacting with json rpc servers.
Expand Down Expand Up @@ -62,14 +64,14 @@ func init() {

// NewClient creates a client from the client type
// defaults to fast http.
func NewClient(clientType ClientType) Client {
func NewClient(handler metrics.Handler, clientType ClientType) Client {
switch clientType {
case FastHTTP:
return NewFastHTTPClient()
return NewFastHTTPClient(handler)
case Resty:
return NewRestyClient()
return NewRestyClient(handler)
default:
return NewRestyClient()
return NewRestyClient(handler)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle unknown ClientType explicitly to prevent unexpected behavior.

In the default case of the switch statement, the function returns NewRestyClient(handler) when an unknown ClientType is provided. This could lead to unintended behavior if an unrecognized ClientType is used. Consider explicitly handling unknown ClientType values by returning an error or defaulting to a safe, well-documented client type, and possibly logging a warning to alert developers.

Comment on lines +67 to +74
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Action Required: Update NewClient Call Sites to Include the handler Parameter

Several instances of NewClient are missing the new handler parameter. Please update these call sites to include handler metrics.Handler to ensure proper functionality and prevent potential issues.

  • ethergo/client/capture_client.go:

    • ethClient := ethclient.NewClient(c)
    • w3Client := w3.NewClient(c)
  • ethergo/backends/geth/geth.go:

    • rawClient := ethclient.NewClient(rpcClient)
    • w3Client := w3.NewClient(rpcClient)
  • ethergo/chain/client/client.go:

    • c.Client = ethclient.NewClient(c.rpcClient)
    • c.w3Client = w3.NewClient(c.rpcClient)
    • ethClient := ethclient.NewClient(rpcClient)
    • w3Client := w3.NewClient(rpcClient)
    • ethClient := ethclient.NewClient(rpcClient)
    • w3Client := w3.NewClient(rpcClient)
  • core/metrics/otlp.go:

    • return otlptracehttp.NewClient(to.httpOptions...), nil
    • return otlptracegrpc.NewClient(to.grpcOptions...), nil
  • contrib/git-changes-action/detector/git/ref.go:

    • client := github.NewClient(tc)

Please ensure that each NewClient invocation includes the required handler parameter.

🔗 Analysis chain

Ensure all call sites of NewClient are updated with the new handler parameter.

The NewClient function signature now includes handler metrics.Handler. Please verify that all calls to NewClient throughout the codebase are updated to pass the new handler argument to prevent compilation errors or unexpected behavior.

Run the following script to find all usages of NewClient:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all call sites of NewClient that need to be updated.

# Search for usage of 'NewClient(' in Go files
rg --type go 'NewClient\('

Length of output: 5031


Script:

#!/bin/bash
# Description: Find all call sites of NewClient that do not include the handler parameter.

# Search for 'NewClient(' in Go files with fewer than expected arguments
rg --type go 'NewClient\s*\(\s*(?!handler\s*,).*?\)'

Length of output: 312


Script:

#!/bin/bash
# Description: Find all call sites of NewClient that include only one argument.

# Search for 'NewClient(' in Go files with only one argument
rg --type go 'NewClient\s*\(\s*[^,()]+\s*\)'

Length of output: 990

}
}

Expand Down
7 changes: 4 additions & 3 deletions services/omnirpc/http/client_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package http_test

import (
"github.com/brianvoe/gofakeit/v6"
. "github.com/stretchr/testify/assert"
"io"
"net/http"
"net/http/httptest"

"github.com/brianvoe/gofakeit/v6"
. "github.com/stretchr/testify/assert"
)

var jsonOptions = &gofakeit.JSONOptions{
Expand Down Expand Up @@ -42,9 +43,9 @@ func (c *HTTPSuite) TestClient() {
}))

req := client.NewRequest()
req.SetContext(c.GetTestContext())
req.SetRequestURI(svr.URL)
req.SetBody(mockBody)
req.SetContext(c.GetTestContext())
for key, val := range headers {
if gofakeit.Bool() {
req.SetHeader(key, val)
Expand Down
Loading
Loading