Skip to content

Commit 071b1c9

Browse files
GGN Engprod Teampraveen-puli
authored andcommitted
Project import generated by Copybara.
FolderOrigin-RevId: /usr/local/google/home/praveenpuli/copybara/temp/folder-destination11731996582883556768/.
1 parent e754b2d commit 071b1c9

File tree

4 files changed

+53115
-52796
lines changed

4 files changed

+53115
-52796
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package grpclog contains gRPC logging utilities useful to binding implementations.
16+
//
17+
// # Usage:
18+
// Pass the following grpc dial options when creating a gRPC client connection:
19+
//
20+
// grpcLoggingDialOpts := []grpc.DialOption{
21+
// grpc.WithChainUnaryInterceptor(grpclog.UnaryClientInterceptor()),
22+
// grpc.WithChainStreamInterceptor(grpclog.StreamClientInterceptor()),
23+
// }
24+
//
25+
// # Where do the logs go?
26+
// RPCs made using connections established with these dial options will be logged to:
27+
// 1. Files in the directory specified by the TEST_UNDECLARED_OUTPUTS_DIR environment variable, if set.
28+
// Otherwise, files in the system temporary directory which is '/tmp' for unix/linux based systems.
29+
// 2. The standard log output at verbosity level 3.
30+
package grpclog
31+
32+
import (
33+
"fmt"
34+
"os"
35+
"path/filepath"
36+
"regexp"
37+
"time"
38+
39+
"golang.org/x/net/context"
40+
41+
log "github.com/golang/glog"
42+
"google.golang.org/grpc"
43+
"google.golang.org/grpc/metadata"
44+
)
45+
46+
var (
47+
grpcTargetRegexp = regexp.MustCompile(`[^a-zA-Z0-9_.-]`)
48+
testOutputsDir = os.Getenv("TEST_UNDECLARED_OUTPUTS_DIR")
49+
)
50+
51+
func init() {
52+
if testOutputsDir == "" {
53+
testOutputsDir = os.TempDir()
54+
}
55+
}
56+
57+
// sanitizeTarget makes the target string safe for use as a filename.
58+
func sanitizeTarget(target string) string {
59+
// Remove common problematic characters for filenames
60+
return grpcTargetRegexp.ReplaceAllString(target, "_")
61+
}
62+
63+
// writeLog writes the log message inline and to the target specific log file in the test outputs directory.
64+
func writeLog(target string, format string, args ...any) {
65+
msg := fmt.Sprintf(format, args...)
66+
log.V(3).Infof("[%s] %s", target, msg)
67+
68+
logFileName := fmt.Sprintf("grpc-target-%s.log", sanitizeTarget(target))
69+
logFilePath := filepath.Join(testOutputsDir, logFileName)
70+
logMsg := fmt.Sprintf("[%s] %s\n", time.Now().Format(time.RFC3339Nano), msg)
71+
if err := appendToFile(logFilePath, logMsg); err != nil {
72+
log.Errorf("Failed to write log (%s) to file (%s): %v", logMsg, logFilePath, err)
73+
}
74+
}
75+
76+
// appendToFile appends the msg string to the file at the given path.
77+
func appendToFile(filePath string, msg string) error {
78+
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
79+
if err != nil {
80+
return err
81+
}
82+
defer f.Close()
83+
_, err = f.WriteString(msg)
84+
return err
85+
}
86+
87+
// UnaryClientInterceptor returns a UnaryClientInterceptor function.
88+
func UnaryClientInterceptor() grpc.UnaryClientInterceptor {
89+
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
90+
target := cc.Target()
91+
92+
md, _ := metadata.FromOutgoingContext(ctx)
93+
94+
writeLog(target, "CALL START: Method: %s, Metadata: %+v, Request: %+v", method, md, req)
95+
96+
startTime := time.Now()
97+
err := invoker(ctx, method, req, reply, cc, opts...)
98+
duration := time.Since(startTime)
99+
100+
status := "OK"
101+
if err != nil {
102+
status = fmt.Sprintf("ERROR: %v", err)
103+
}
104+
writeLog(target, "CALL END: Method: %s, Duration: %s, Status: %s, Reply: %+v", method, duration, status, reply)
105+
106+
return err
107+
}
108+
}
109+
110+
// StreamClientInterceptor returns a StreamClientInterceptor function.
111+
func StreamClientInterceptor() grpc.StreamClientInterceptor {
112+
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
113+
target := cc.Target()
114+
115+
md, _ := metadata.FromOutgoingContext(ctx)
116+
117+
writeLog(target, "CALL START: Method: %s, Metadata: %+v, StreamDesc: %+v", method, md, desc)
118+
119+
clientStream, err := streamer(ctx, desc, cc, method, opts...)
120+
if err != nil {
121+
writeLog(target, "CALL END: Failed to start stream (Method: %s, Metadata: %+v, StreamDesc: %+v): %v", method, md, desc, err)
122+
return nil, err
123+
}
124+
125+
return &wrappedClientStream{
126+
ClientStream: clientStream,
127+
method: method,
128+
target: target,
129+
}, nil
130+
}
131+
}
132+
133+
// wrappedClientStream wraps grpc.ClientStream to log messages.
134+
type wrappedClientStream struct {
135+
grpc.ClientStream
136+
method string
137+
target string
138+
}
139+
140+
func (w *wrappedClientStream) SendMsg(m any) error {
141+
err := w.ClientStream.SendMsg(m)
142+
if err != nil {
143+
writeLog(w.target, "(Stream %s) SendMsg error: %v", w.method, err)
144+
} else {
145+
writeLog(w.target, "(Stream %s) SendMsg success: %+v", w.method, m)
146+
}
147+
return err
148+
}
149+
150+
func (w *wrappedClientStream) RecvMsg(m any) error {
151+
err := w.ClientStream.RecvMsg(m)
152+
if err != nil {
153+
writeLog(w.target, "(Stream %s) RecvMsg error: %v", w.method, err)
154+
} else {
155+
writeLog(w.target, "(Stream %s) RecvMsg success: %+v", w.method, m)
156+
}
157+
return err
158+
}
159+
160+
func (w *wrappedClientStream) Header() (metadata.MD, error) {
161+
md, err := w.ClientStream.Header()
162+
if err != nil {
163+
writeLog(w.target, "(Stream %s) Header error: %v", w.method, err)
164+
} else {
165+
writeLog(w.target, "(Stream %s) Header success: %+v", w.method, md)
166+
}
167+
return md, err
168+
}
169+
170+
func (w *wrappedClientStream) Trailer() metadata.MD {
171+
md := w.ClientStream.Trailer()
172+
writeLog(w.target, "(Stream %s) Trailer: %+v", w.method, md)
173+
return md
174+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package grpclog
16+
17+
import (
18+
"io"
19+
"os"
20+
"path/filepath"
21+
"sort"
22+
"strings"
23+
"testing"
24+
25+
"golang.org/x/net/context"
26+
27+
"github.com/openconfig/ondatra/binding/grpcutil/testservice"
28+
tgrpcpb "github.com/openconfig/ondatra/binding/grpcutil/testservice/gen"
29+
tpb "github.com/openconfig/ondatra/binding/grpcutil/testservice/gen"
30+
"google.golang.org/grpc"
31+
"google.golang.org/grpc/metadata"
32+
)
33+
34+
func TestSanitizeTarget(t *testing.T) {
35+
tests := []struct {
36+
name string
37+
target string
38+
want string
39+
}{
40+
{
41+
name: "valid target",
42+
target: "localhost:1234",
43+
want: "localhost_1234",
44+
},
45+
{
46+
name: "target with special chars",
47+
target: "unix:///tmp/grpc.sock",
48+
want: "unix____tmp_grpc.sock",
49+
},
50+
{
51+
name: "empty target",
52+
target: "",
53+
want: "",
54+
},
55+
}
56+
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
if got := sanitizeTarget(tt.target); got != tt.want {
60+
t.Errorf("sanitizeTarget(%q) = %q, want %q", tt.target, got, tt.want)
61+
}
62+
})
63+
}
64+
}
65+
66+
type testServer struct {
67+
tgrpcpb.UnimplementedTestServer
68+
}
69+
70+
func (*testServer) SendUnary(context.Context, *tpb.TestRequest) (*tpb.TestResponse, error) {
71+
return &tpb.TestResponse{}, nil
72+
}
73+
74+
func (*testServer) SendStream(stream tgrpcpb.Test_SendStreamServer) error {
75+
for {
76+
_, err := stream.Recv()
77+
if err == io.EOF {
78+
return stream.Send(&tpb.TestResponse{})
79+
}
80+
if err != nil {
81+
return err
82+
}
83+
}
84+
}
85+
86+
func TestInterceptors(t *testing.T) {
87+
ctx := context.Background()
88+
opts := []grpc.DialOption{
89+
grpc.WithChainUnaryInterceptor(UnaryClientInterceptor()),
90+
grpc.WithChainStreamInterceptor(StreamClientInterceptor()),
91+
}
92+
ts := testservice.Start(ctx, t, &testServer{}, opts...)
93+
defer ts.Stop()
94+
95+
md := metadata.Pairs("key", "value")
96+
ctx = metadata.NewOutgoingContext(ctx, md)
97+
98+
ts.MustSendUnary(ctx, t, "unary")
99+
stream := ts.MustSendStream(ctx, t)
100+
stream.MustSend(t, "stream")
101+
stream.CloseSend()
102+
if !stream.MustRecv(t) {
103+
t.Errorf("Expected one message from stream, got none")
104+
}
105+
if stream.MustRecv(t) {
106+
t.Errorf("Expected EOF from stream, but received a message")
107+
}
108+
109+
logDir := testOutputsDir
110+
ents, err := os.ReadDir(logDir)
111+
if err != nil {
112+
t.Fatalf("Failed to read log dir: %v", err)
113+
}
114+
var logFiles []string
115+
for _, ent := range ents {
116+
if strings.HasPrefix(ent.Name(), "grpc-target-") && strings.HasSuffix(ent.Name(), ".log") {
117+
logFiles = append(logFiles, ent.Name())
118+
}
119+
}
120+
if len(logFiles) == 0 {
121+
t.Fatalf("Got 0 log files, want at least 1")
122+
}
123+
sort.Strings(logFiles)
124+
125+
var gotLogs string
126+
for _, f := range logFiles {
127+
logFile := filepath.Join(logDir, f)
128+
data, err := os.ReadFile(logFile)
129+
if err != nil {
130+
t.Fatalf("Failed to read log file %s: %v", f, err)
131+
}
132+
gotLogs += string(data)
133+
}
134+
135+
wantSubstrings := []string{
136+
// Unary call logs
137+
`CALL START: Method: /testservice.Test/SendUnary, Metadata: map[key:[value]], Request: message:"unary"`,
138+
`CALL END: Method: /testservice.Test/SendUnary`,
139+
// Stream call logs
140+
`CALL START: Method: /testservice.Test/SendStream, Metadata: map[key:[value]]`,
141+
`(Stream /testservice.Test/SendStream) SendMsg success: message:"stream"`,
142+
`(Stream /testservice.Test/SendStream) RecvMsg success:`,
143+
`(Stream /testservice.Test/SendStream) RecvMsg error: EOF`,
144+
}
145+
for _, want := range wantSubstrings {
146+
if !strings.Contains(gotLogs, want) {
147+
t.Errorf("Log files %v do not contain expected substring %q; combined content:\n%s", logFiles, want, gotLogs)
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)