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: updating filter result to add decorations #236

Merged
merged 4 commits into from
Jul 25, 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
10 changes: 10 additions & 0 deletions sdk/filter/result/filter_result.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
package result

type KeyValueString struct {
Key string
Value string
}

type Decorations struct {
RequestHeaderInjections []KeyValueString
}

type FilterResult struct {
Block bool
ResponseStatusCode int32
ResponseMessage string
Decorations *Decorations
}
15 changes: 10 additions & 5 deletions sdk/instrumentation/google.golang.org/grpc/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,22 @@ import (
"google.golang.org/grpc/test/bufconn"
)

var _ helloworld.GreeterServer = server{}
// type assertion
var _ helloworld.GreeterServer = (*server)(nil)

type server struct {
err error
replyHeader metadata.MD
replyTrailer metadata.MD
err error
requestHeader metadata.MD
replyHeader metadata.MD
replyTrailer metadata.MD
*helloworld.UnimplementedGreeterServer
}

func (s server) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
func (s *server) SayHello(ctx context.Context, req *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
var reply *helloworld.HelloReply
if md, ok := metadata.FromIncomingContext(ctx); ok {
s.requestHeader = md
}
if s.err == nil {
reply = &helloworld.HelloReply{Message: fmt.Sprintf("Hello %s", req.GetName())}
}
Expand Down
9 changes: 9 additions & 0 deletions sdk/instrumentation/google.golang.org/grpc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
internalconfig "github.com/hypertrace/goagent/sdk/internal/config"
"github.com/hypertrace/goagent/sdk/internal/container"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/stats"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -109,6 +110,14 @@ func wrapHandler(
filterResult := filter.Evaluate(span)
if filterResult.Block {
return nil, status.Error(StatusCode(int(filterResult.ResponseStatusCode)), StatusText(int(filterResult.ResponseStatusCode)))
} else if filterResult.Decorations != nil {
if md, ok := metadata.FromIncomingContext(ctx); ok {
for _, header := range filterResult.Decorations.RequestHeaderInjections {
md.Append(header.Key, header.Value)
span.SetAttribute("rpc.request.metadata."+header.Key, header.Value)
}
ctx = metadata.NewIncomingContext(ctx, md)
}
}

res, err := delegateHandler(ctx, req)
Expand Down
124 changes: 124 additions & 0 deletions sdk/instrumentation/google.golang.org/grpc/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,130 @@ func TestServerInterceptorFilterWithMaxProcessingBodyLen(t *testing.T) {
assert.NoError(t, err)
}

func TestServerInterceptorFilterDecorations(t *testing.T) {
var spans []*mock.Span
mockInterceptor := makeMockUnaryServerInterceptor(&spans)
// wrap interceptor with filter
s := grpc.NewServer(
grpc.UnaryInterceptor(
WrapUnaryServerInterceptor(mockInterceptor, mock.SpanFromContext, &Options{Filter: mock.Filter{
Evaluator: func(span sdk.Span) result.FilterResult {
return result.FilterResult{Block: false, Decorations: &result.Decorations{
RequestHeaderInjections: []result.KeyValueString{
{
Key: "injected-header",
Value: "injected-value",
},
},
}}
},
}}, nil),
),
)
defer s.Stop()

mockServer := &server{}
helloworld.RegisterGreeterServer(s, mockServer)

dialer := createDialer(s)

ctx := context.Background()
conn, err := grpc.DialContext(
ctx,
"bufnet",
grpc.WithContextDialer(dialer),
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("failed to dial bufnet: %v", err)
}
defer conn.Close()

client := helloworld.NewGreeterClient(conn)

ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("test_key", "test_value"))
_, err = client.SayHello(ctx, &helloworld.HelloRequest{
Name: "Pupo",
})
assert.NoError(t, err)

md := mockServer.requestHeader
// assert original header
val, found := md["test_key"]
assert.True(t, found)
assert.Equal(t, "test_value", val[0])

// assert injected header
val, found = md["injected-header"]
assert.True(t, found)
assert.Equal(t, "injected-value", val[0])
assert.NoError(t, err)

assert.Equal(t, 1, len(spans))
span := spans[0]
spanAttributePresent := false
span.GetAttributes().Iterate(func(key string, value interface{}) bool {
if key == "rpc.request.metadata.injected-header" {
assert.Equal(t, "injected-value", value.(string))
spanAttributePresent = true
return false
}
return true
})
assert.True(t, spanAttributePresent)
}

func TestServerInterceptorFilterEmptyDecorations(t *testing.T) {
spans := []*mock.Span{}
mockInterceptor := makeMockUnaryServerInterceptor(&spans)
// wrap interceptor with filter
s := grpc.NewServer(
grpc.UnaryInterceptor(
WrapUnaryServerInterceptor(mockInterceptor, mock.SpanFromContext, &Options{Filter: mock.Filter{
Evaluator: func(span sdk.Span) result.FilterResult {
return result.FilterResult{Block: false, Decorations: &result.Decorations{}}
},
}}, nil),
),
)
defer s.Stop()

mockServer := &server{}
helloworld.RegisterGreeterServer(s, mockServer)

dialer := createDialer(s)

ctx := context.Background()
conn, err := grpc.DialContext(
ctx,
"bufnet",
grpc.WithContextDialer(dialer),
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("failed to dial bufnet: %v", err)
}
defer conn.Close()

client := helloworld.NewGreeterClient(conn)

ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("test_key", "test_value"))
_, err = client.SayHello(ctx, &helloworld.HelloRequest{
Name: "Pupo",
})
assert.NoError(t, err)

md := mockServer.requestHeader
// assert original header
val, found := md["test_key"]
assert.True(t, found)
assert.Equal(t, "test_value", val[0])

assert.Equal(t, 1, len(spans))
}

func TestServerHandlerHelloWorldSuccess(t *testing.T) {
defer internalconfig.ResetConfig()

Expand Down
4 changes: 2 additions & 2 deletions sdk/instrumentation/net/http/attributes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestSetScalarAttributeSuccess(t *testing.T) {
h := http.Header{}
h.Set("key_1", "value_1")
span := mock.NewSpan()
SetAttributesFromHeaders("request", headerMapAccessor{h}, span)
SetAttributesFromHeaders("request", &headerMapAccessor{h}, span)
assert.Equal(t, "value_1", span.ReadAttribute("http.request.header.key_1").(string))

_ = span.ReadAttribute("container_id") // needed in containarized envs
Expand All @@ -25,7 +25,7 @@ func TestSetMultivalueAttributeSuccess(t *testing.T) {
h.Add("key_1", "value_2")

span := mock.NewSpan()
SetAttributesFromHeaders("response", headerMapAccessor{h}, span)
SetAttributesFromHeaders("response", &headerMapAccessor{h}, span)

assert.Equal(t, "value_1", span.ReadAttribute("http.response.header.key_1[0]").(string))
assert.Equal(t, "value_2", span.ReadAttribute("http.response.header.key_1[1]").(string))
Expand Down
10 changes: 5 additions & 5 deletions sdk/instrumentation/net/http/contenttype_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

func TestRecordingDecisionReturnsFalseOnNoContentType(t *testing.T) {
assert.Equal(t, false, ShouldRecordBodyOfContentType(headerMapAccessor{http.Header{"A": []string{"B"}}}))
assert.Equal(t, false, ShouldRecordBodyOfContentType(&headerMapAccessor{http.Header{"A": []string{"B"}}}))
}

func TestRecordingDecisionSuccessOnHeaderSet(t *testing.T) {
Expand All @@ -32,7 +32,7 @@ func TestRecordingDecisionSuccessOnHeaderSet(t *testing.T) {
for _, tCase := range tCases {
h := http.Header{}
h.Set("Content-Type", tCase.contentType)
assert.Equal(t, tCase.shouldRecord, ShouldRecordBodyOfContentType(headerMapAccessor{h}))
assert.Equal(t, tCase.shouldRecord, ShouldRecordBodyOfContentType(&headerMapAccessor{h}))
}
}

Expand All @@ -56,7 +56,7 @@ func TestRecordingDecisionSuccessOnHeaderAdd(t *testing.T) {
for _, header := range tCase.contentTypes {
h.Add("Content-Type", header)
}
assert.Equal(t, tCase.shouldRecord, ShouldRecordBodyOfContentType(headerMapAccessor{h}))
assert.Equal(t, tCase.shouldRecord, ShouldRecordBodyOfContentType(&headerMapAccessor{h}))
}
}

Expand All @@ -80,7 +80,7 @@ func TestXMLRecordingDecisionSuccessOnHeaderAdd(t *testing.T) {
for _, header := range tCase.contentTypes {
h.Add("Content-Type", header)
}
assert.Equal(t, tCase.shouldRecord, ShouldRecordBodyOfContentType(headerMapAccessor{h}))
assert.Equal(t, tCase.shouldRecord, ShouldRecordBodyOfContentType(&headerMapAccessor{h}))
}
internalconfig.ResetConfig()
}
Expand All @@ -99,6 +99,6 @@ func TestHasMultiPartFormDataContentTypeHeader(t *testing.T) {
for _, tCase := range tCases {
h := http.Header{}
h.Set("Content-Type", tCase.contentType)
assert.Equal(t, tCase.isMultiPartFormData, HasMultiPartFormDataContentTypeHeader(headerMapAccessor{h}))
assert.Equal(t, tCase.isMultiPartFormData, HasMultiPartFormDataContentTypeHeader(&headerMapAccessor{h}))
}
}
19 changes: 13 additions & 6 deletions sdk/instrumentation/net/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func WrapHandler(delegate http.Handler, spanFromContext sdk.SpanFromContext, opt
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
span := h.spanFromContextRetriever(ctx)
headersAccessor := NewHeaderMapAccessor(r.Header)

h.mh.AddToRequestCount(1, r)

Expand Down Expand Up @@ -77,19 +78,19 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

// Sets an attribute per each request header.
if h.dataCaptureConfig.HttpHeaders.Request.Value {
SetAttributesFromHeaders("request", NewHeaderMapAccessor(r.Header), span)
SetAttributesFromHeaders("request", headersAccessor, span)
}

// nil check for body is important as this block turns the body into another
// object that isn't nil and that will leverage the "Observer effect".
if r.Body != nil && h.dataCaptureConfig.HttpBody.Request.Value && ShouldRecordBodyOfContentType(headerMapAccessor{r.Header}) {
if r.Body != nil && h.dataCaptureConfig.HttpBody.Request.Value && ShouldRecordBodyOfContentType(headersAccessor) {
body, err := io.ReadAll(r.Body)
if err != nil {
return
}
defer r.Body.Close()

isMultipartFormDataBody := HasMultiPartFormDataContentTypeHeader(headerMapAccessor{r.Header})
isMultipartFormDataBody := HasMultiPartFormDataContentTypeHeader(headersAccessor)

// Only records the body if it is not empty and the content type
// header is not streamable
Expand All @@ -106,23 +107,29 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if filterResult.Block {
w.WriteHeader(int(filterResult.ResponseStatusCode))
return
} else if filterResult.Decorations != nil {
for _, header := range filterResult.Decorations.RequestHeaderInjections {
headersAccessor.AddHeader(header.Key, header.Value)
span.SetAttribute("http.request.header."+header.Key, header.Value)
}
}

// create http.ResponseWriter interceptor for tracking status code
wi := &rwInterceptor{w: w, statusCode: 200}

// tag found status code on exit
defer func() {
responseHeadersAccessor := NewHeaderMapAccessor(wi.Header())
if h.dataCaptureConfig.HttpBody.Response.Value &&
len(wi.body) > 0 &&
ShouldRecordBodyOfContentType(headerMapAccessor{wi.Header()}) {
ShouldRecordBodyOfContentType(responseHeadersAccessor) {
setTruncatedBodyAttribute("response", wi.body, int(h.dataCaptureConfig.BodyMaxSizeBytes.Value), span,
HasMultiPartFormDataContentTypeHeader(headerMapAccessor{wi.Header()}))
HasMultiPartFormDataContentTypeHeader(responseHeadersAccessor))
}

if h.dataCaptureConfig.HttpHeaders.Response.Value {
// Sets an attribute per each response header.
SetAttributesFromHeaders("response", headerMapAccessor{wi.Header()}, span)
SetAttributesFromHeaders("response", responseHeadersAccessor, span)
}
}()

Expand Down
Loading
Loading