diff --git a/metadata/metadata.go b/metadata/metadata.go index 1e9485fd6e2..6c01a9b359c 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -90,6 +90,21 @@ func Pairs(kv ...string) MD { return md } +// String implements the Stringer interface for pretty-printing a MD. +// Ordering of the values is non-deterministic as it ranges over a map. +func (md MD) String() string { + var sb strings.Builder + fmt.Fprintf(&sb, "MD{") + for k, v := range md { + if sb.Len() > 3 { + fmt.Fprintf(&sb, ", ") + } + fmt.Fprintf(&sb, "%s=[%s]", k, strings.Join(v, ", ")) + } + fmt.Fprintf(&sb, "}") + return sb.String() +} + // Len returns the number of items in md. func (md MD) Len() int { return len(md) diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index fbee086fb91..6753764b9ba 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -22,6 +22,7 @@ import ( "context" "reflect" "strconv" + "strings" "testing" "time" @@ -338,6 +339,26 @@ func (s) TestAppendToOutgoingContext_FromKVSlice(t *testing.T) { } } +func TestStringerMD(t *testing.T) { + for _, test := range []struct { + md MD + want []string + }{ + {MD{}, []string{"MD{}"}}, + {MD{"k1": []string{}}, []string{"MD{k1=[]}"}}, + {MD{"k1": []string{"v1", "v2"}}, []string{"MD{k1=[v1, v2]}"}}, + {MD{"k1": []string{"v1"}}, []string{"MD{k1=[v1]}"}}, + {MD{"k1": []string{"v1", "v2"}, "k2": []string{}, "k3": []string{"1", "2", "3"}}, []string{"MD{", "k1=[v1, v2]", "k2=[]", "k3=[1, 2, 3]", "}"}}, + } { + got := test.md.String() + for _, want := range test.want { + if !strings.Contains(got, want) { + t.Fatalf("Metadata string %q is missing %q", got, want) + } + } + } +} + // Old/slow approach to adding metadata to context func Benchmark_AddingMetadata_ContextManipulationApproach(b *testing.B) { // TODO: Add in N=1-100 tests once Go1.6 support is removed. diff --git a/peer/peer.go b/peer/peer.go index a821ff9b2b7..499a49c8c1c 100644 --- a/peer/peer.go +++ b/peer/peer.go @@ -22,7 +22,9 @@ package peer import ( "context" + "fmt" "net" + "strings" "google.golang.org/grpc/credentials" ) @@ -39,6 +41,34 @@ type Peer struct { AuthInfo credentials.AuthInfo } +// String ensures the Peer types implements the Stringer interface in order to +// allow to print a context with a peerKey value effectively. +func (p *Peer) String() string { + if p == nil { + return "Peer" + } + sb := &strings.Builder{} + sb.WriteString("Peer{") + if p.Addr != nil { + fmt.Fprintf(sb, "Addr: '%s', ", p.Addr.String()) + } else { + fmt.Fprintf(sb, "Addr: , ") + } + if p.LocalAddr != nil { + fmt.Fprintf(sb, "LocalAddr: '%s', ", p.LocalAddr.String()) + } else { + fmt.Fprintf(sb, "LocalAddr: , ") + } + if p.AuthInfo != nil { + fmt.Fprintf(sb, "AuthInfo: '%s'", p.AuthInfo.AuthType()) + } else { + fmt.Fprintf(sb, "AuthInfo: ") + } + sb.WriteString("}") + + return sb.String() +} + type peerKey struct{} // NewContext creates a new context with peer information attached. diff --git a/peer/peer_test.go b/peer/peer_test.go new file mode 100644 index 00000000000..45240475eec --- /dev/null +++ b/peer/peer_test.go @@ -0,0 +1,103 @@ +/* + * + * Copyright 2024 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package peer + +import ( + "context" + "fmt" + "testing" + + "google.golang.org/grpc/credentials" +) + +// A struct that implements AuthInfo interface and implements CommonAuthInfo() method. +type testAuthInfo struct { + credentials.CommonAuthInfo +} + +func (ta testAuthInfo) AuthType() string { + return fmt.Sprintf("testAuthInfo-%d", ta.SecurityLevel) +} + +type addr struct { + ipAddress string +} + +func (addr) Network() string { return "" } + +func (a *addr) String() string { return a.ipAddress } + +func TestPeerStringer(t *testing.T) { + testCases := []struct { + name string + peer *Peer + want string + }{ + { + name: "+Addr-LocalAddr+ValidAuth", + peer: &Peer{Addr: &addr{"example.com:1234"}, AuthInfo: testAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}}, + want: "Peer{Addr: 'example.com:1234', LocalAddr: , AuthInfo: 'testAuthInfo-3'}", + }, + { + name: "+Addr+LocalAddr+ValidAuth", + peer: &Peer{Addr: &addr{"example.com:1234"}, LocalAddr: &addr{"example.com:1234"}, AuthInfo: testAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}}, + want: "Peer{Addr: 'example.com:1234', LocalAddr: 'example.com:1234', AuthInfo: 'testAuthInfo-3'}", + }, + { + name: "+Addr-LocalAddr+emptyAuth", + peer: &Peer{Addr: &addr{"1.2.3.4:1234"}, AuthInfo: testAuthInfo{credentials.CommonAuthInfo{}}}, + want: "Peer{Addr: '1.2.3.4:1234', LocalAddr: , AuthInfo: 'testAuthInfo-0'}", + }, + { + name: "-Addr-LocalAddr+emptyAuth", + peer: &Peer{AuthInfo: testAuthInfo{}}, + want: "Peer{Addr: , LocalAddr: , AuthInfo: 'testAuthInfo-0'}", + }, + { + name: "zeroedPeer", + peer: &Peer{}, + want: "Peer{Addr: , LocalAddr: , AuthInfo: }", + }, + { + name: "nilPeer", + peer: nil, + want: "Peer", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := NewContext(context.Background(), tc.peer) + p, ok := FromContext(ctx) + if !ok { + t.Fatalf("Unable to get peer from context") + } + if p.String() != tc.want { + t.Fatalf("Error using peer String(): expected %q, got %q", tc.want, p.String()) + } + }) + } +} + +func TestPeerStringerOnContext(t *testing.T) { + ctx := NewContext(context.Background(), &Peer{Addr: &addr{"1.2.3.4:1234"}, AuthInfo: testAuthInfo{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}}) + want := "context.Background.WithValue(type peer.peerKey, val Peer{Addr: '1.2.3.4:1234', LocalAddr: , AuthInfo: 'testAuthInfo-3'})" + if got := fmt.Sprintf("%v", ctx); got != want { + t.Fatalf("Unexpected stringer output, got: %q; want: %q", got, want) + } +}