Repository: googleapis/gapic-generator-go
Pull request #1577 (2024-11-01 20:31:48)

feat: add logging support to generated clients
This change instruments all generated clients with logging. In order to reduce the amount of codegen this would take I refactored some common logic about executing the request and parsing the response into a common helper. This change should reduce LoC for generated clients as a nice side-effect.

codyoss: Requires https://github.com/googleapis/gax-go/pull/380 and https://github.com/googleapis/google-api-go-client/pull/2854 be rolled out to all clients first

Patch:
diff --git a/internal/gengapic/gengapic.go b/internal/gengapic/gengapic.go
index 151b8ef..eb91d28 100644
--- a/internal/gengapic/gengapic.go
+++ b/internal/gengapic/gengapic.go
@@ -232,6 +232,10 @@ func (g *generator) genAndCommitHelpers(scopes []string) error {
 	g.reset()
 	g.imports[pbinfo.ImportSpec{Path: "context"}] = true
 	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/option"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "github.com/googleapis/gax-go/v2/internallog"}] = true
+
+	p("const serviceName = %q", g.serviceConfig.GetName())
+	p("")
 
 	p("// For more information on implementing a client constructor hook, see")
 	p("// https://github.com/googleapis/google-cloud-go/wiki/Customizing-constructors.")
@@ -257,6 +261,64 @@ func (g *generator) genAndCommitHelpers(scopes []string) error {
 	}
 	p("  }")
 	p("}")
+	p("")
+	if containsTransport(g.opts.transports, rest) {
+		g.imports[pbinfo.ImportSpec{Path: "io"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "log/slog"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "net/http"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/googleapi"}] = true
+
+		p("func executeHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger,	body []byte, rpc string) ([]byte, error) {")
+		p(`  logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))`)
+		p("  resp, err := client.Do(req)")
+		p("  if err != nil{")
+		p("    return nil, err")
+		p("  }")
+		p("  defer resp.Body.Close()")
+		p("  buf, err := io.ReadAll(resp.Body)")
+		p("  if err != nil {")
+		p("    return nil, err")
+		p("  }")
+		p(`  logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, buf))`)
+		p("  if err = googleapi.CheckResponse(resp); err != nil {")
+		p("    return nil, err")
+		p("  }")
+		p("  return buf, nil")
+		p("}")
+		p("")
+
+		p("func executeStreamingHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger, body []byte, rpc string) (*http.Response, error) {")
+		p(`  logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))`)
+		p("  resp, err := client.Do(req)")
+		p("  if err != nil{")
+		p("    return nil, err")
+		p("  }")
+		p(`  logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, nil))`)
+		p("  if err = googleapi.CheckResponse(resp); err != nil {")
+		p("    return nil, err")
+		p("  }")
+		p("  return resp, nil")
+		p("}")
+		p("")
+	}
+
+	if containsTransport(g.opts.transports, grpc) {
+		g.imports[pbinfo.ImportSpec{Path: "github.com/googleapis/gax-go/v2/internallog/grpclog"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/grpc"}] = true
+		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/protobuf/proto"}] = true
+
+		p("func executeRPC[I proto.Message, O proto.Message](ctx context.Context, fn func(context.Context, I, ...grpc.CallOption) (O, error), req I, opts []grpc.CallOption, logger *slog.Logger, rpc string) (O, error) {")
+		p("  var zero O")
+		p(`  logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", grpclog.ProtoMessageRequest(ctx, req))`)
+		p("  resp, err := fn(ctx, req, opts...)")
+		p("  if err != nil {")
+		p("    return zero, err")
+		p("  }")
+		p(`  logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", grpclog.ProtoMessageResponse(resp))`)
+		p("  return resp, err")
+		p("}")
+		p("")
+	}
 
 	outFile := filepath.Join(g.opts.outDir, "helpers.go")
 	g.commit(outFile, g.opts.pkgName)
diff --git a/internal/gengapic/gengrpc.go b/internal/gengapic/gengrpc.go
index 9857e11..51f1671 100644
--- a/internal/gengapic/gengrpc.go
+++ b/internal/gengapic/gengrpc.go
@@ -162,7 +162,7 @@ func (g *generator) emptyUnaryGRPCCall(servName string, m *descriptorpb.MethodDe
 func (g *generator) grpcStubCall(method *descriptorpb.MethodDescriptorProto) string {
 	service := g.descInfo.ParentElement[method]
 	stub := pbinfo.ReduceServName(service.GetName(), g.opts.pkgName)
-	return fmt.Sprintf("c.%s.%s(ctx, req, settings.GRPC...)", grpcClientField(stub), method.GetName())
+	return fmt.Sprintf("executeRPC(ctx, c.%s.%s, req, settings.GRPC, c.logger, %q)", grpcClientField(stub), method.GetName(), method.GetName())
 }
 
 func (g *generator) grpcClientOptions(serv *descriptorpb.ServiceDescriptorProto, servName string) error {
@@ -295,6 +295,8 @@ func (g *generator) grpcClientInit(serv *descriptorpb.ServiceDescriptorProto, se
 
 	p("// The x-goog-* metadata to be sent with each request.")
 	p("xGoogHeaders []string")
+	p("")
+	p("logger *slog.Logger")
 
 	p("}")
 	p("")
@@ -303,6 +305,7 @@ func (g *generator) grpcClientInit(serv *descriptorpb.ServiceDescriptorProto, se
 	g.imports[imp] = true
 
 	g.grpcClientUtilities(serv, servName, imp, hasRPCForLRO)
+	g.imports[pbinfo.ImportSpec{Path: "log/slog"}] = true
 }
 
 func (g *generator) grpcClientUtilities(serv *descriptorpb.ServiceDescriptorProto, servName string, imp pbinfo.ImportSpec, hasRPCForLRO bool) {
@@ -337,6 +340,7 @@ func (g *generator) grpcClientUtilities(serv *descriptorpb.ServiceDescriptorProt
 	p("    connPool:    connPool,")
 	p("    %s: %s.New%sClient(connPool),", grpcClientField(servName), imp.Name, serv.GetName())
 	p("    CallOptions: &client.CallOptions,")
+	p("    logger: internaloption.GetLogger(opts),")
 	g.mixinStubsInit()
 	p("")
 	p("  }")
diff --git a/internal/gengapic/genrest.go b/internal/gengapic/genrest.go
index b9bf409..5230925 100644
--- a/internal/gengapic/genrest.go
+++ b/internal/gengapic/genrest.go
@@ -91,6 +91,8 @@ func (g *generator) restClientInit(serv *descriptorpb.ServiceDescriptorProto, se
 	p("")
 	p("  // Points back to the CallOptions field of the containing %sClient", servName)
 	p("  CallOptions **%sCallOptions", servName)
+	p("")
+	p("  logger *slog.Logger")
 	p("}")
 	p("")
 	g.restClientUtilities(serv, servName, hasRPCForLRO)
@@ -98,6 +100,7 @@ func (g *generator) restClientInit(serv *descriptorpb.ServiceDescriptorProto, se
 	g.imports[pbinfo.ImportSpec{Path: "net/http"}] = true
 	g.imports[pbinfo.ImportSpec{Name: "httptransport", Path: "google.golang.org/api/transport/http"}] = true
 	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/option/internaloption"}] = true
+	g.imports[pbinfo.ImportSpec{Path: "log/slog"}] = true
 }
 
 func (g *generator) genRESTMethods(serv *descriptorpb.ServiceDescriptorProto, servName string) error {
@@ -163,6 +166,7 @@ func (g *generator) restClientUtilities(serv *descriptorpb.ServiceDescriptorProt
 	p("        endpoint: endpoint,")
 	p("        httpClient: httpClient,")
 	p("        CallOptions: &callOpts,")
+	p("        logger: internaloption.GetLogger(opts),")
 	p("    }")
 	p("    c.setGoogleClientInfo()")
 	p("")
@@ -617,7 +621,7 @@ func (g *generator) serverStreamRESTCall(servName string, s *descriptorpb.Servic
 	// rest-client method
 	p("func (c *%s) %s(ctx context.Context, req *%s.%s, opts ...gax.CallOption) (%s.%s_%sClient, error) {",
 		lowcaseServName, m.GetName(), inSpec.Name, inType.GetName(), servSpec.Name, s.GetName(), m.GetName())
-	body := "nil"
+	body, logBody := "nil", "nil"
 	verb := strings.ToUpper(info.verb)
 
 	// Marshal body for HTTP methods that take a body.
@@ -638,6 +642,7 @@ func (g *generator) serverStreamRESTCall(servName string, s *descriptorpb.Servic
 		p("")
 
 		body = "bytes.NewReader(jsonReq)"
+		logBody = "jsonReq"
 		g.imports[pbinfo.ImportSpec{Path: "bytes"}] = true
 		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/protobuf/encoding/protojson"}] = true
 	}
@@ -658,15 +663,11 @@ func (g *generator) serverStreamRESTCall(servName string, s *descriptorpb.Servic
 	p("  httpReq = httpReq.WithContext(ctx)")
 	p("  httpReq.Header = headers")
 	p("")
-	p("  httpRsp, err := c.httpClient.Do(httpReq)")
+	p("  httpRsp, err := executeStreamingHTTPRequest(ctx, c.httpClient, httpReq, c.logger, %s, %q)", logBody, m.GetName())
 	p("  if err != nil{")
 	p("   return err")
 	p("  }")
 	p("")
-	p("  if err = googleapi.CheckResponse(httpRsp); err != nil {")
-	p("    return err")
-	p("  }")
-	p("")
 	p("  streamClient = &%s{", streamClient)
 	p("    ctx: ctx,")
 	p("    md: metadata.MD(httpRsp.Header),")
@@ -679,8 +680,6 @@ func (g *generator) serverStreamRESTCall(servName string, s *descriptorpb.Servic
 	p("}")
 	p("")
 
-	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/googleapi"}] = true
-
 	// server-stream wrapper client
 	p("// %s is the stream client used to consume the server stream created by", streamClient)
 	p("// the REST implementation of %s.", m.GetName())
@@ -800,10 +799,11 @@ func (g *generator) pagingRESTCall(servName string, m *descriptorpb.MethodDescri
 	p("it := &%s{}", pt.iterTypeName)
 	p("req = proto.Clone(req).(*%s.%s)", inSpec.Name, inType.GetName())
 
-	maybeReqBytes := "nil"
+	maybeReqBytes, logBody := "nil", "nil"
 	if info.body != "" {
 		g.protoJSONMarshaler()
 		maybeReqBytes = "bytes.NewReader(jsonReq)"
+		logBody = "jsonReq"
 		g.imports[pbinfo.ImportSpec{Path: "bytes"}] = true
 	}
 
@@ -835,21 +835,10 @@ func (g *generator) pagingRESTCall(servName string, m *descriptorpb.MethodDescri
 	// TODO: Should this http.Request use WithContext?
 	p("    httpReq.Header = headers")
 	p("")
-	p("    httpRsp, err := c.httpClient.Do(httpReq)")
+	p("    buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, %s, %q)", logBody, m.GetName())
 	p("    if err != nil{")
 	p(`     return err`)
 	p("    }")
-	p("    defer httpRsp.Body.Close()")
-	p("")
-	p("    if err = googleapi.CheckResponse(httpRsp); err != nil {")
-	p(`      return err`)
-	p("    }")
-	p("")
-	p("    buf, err := io.ReadAll(httpRsp.Body)")
-	p("    if err != nil {")
-	p(`      return err`)
-	p("    }")
-	p("")
 	p("    if err := unm.Unmarshal(buf, resp); err != nil {")
 	p("      return err")
 	p("    }")
@@ -868,10 +857,8 @@ func (g *generator) pagingRESTCall(servName string, m *descriptorpb.MethodDescri
 	p("}")
 
 	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/iterator"}] = true
-	g.imports[pbinfo.ImportSpec{Path: "io"}] = true
 	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/protobuf/proto"}] = true
 	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/protobuf/encoding/protojson"}] = true
-	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/googleapi"}] = true
 	g.imports[inSpec] = true
 	g.imports[outSpec] = true
 
@@ -916,7 +903,7 @@ func (g *generator) lroRESTCall(servName string, m *descriptorpb.MethodDescripto
 	// TODO(noahdietz): handle deadlines?
 	// TODO(noahdietz): handle calloptions
 
-	body := "nil"
+	body, logBody := "nil", "nil"
 	verb := strings.ToUpper(info.verb)
 
 	// Marshal body for HTTP methods that take a body.
@@ -937,6 +924,7 @@ func (g *generator) lroRESTCall(servName string, m *descriptorpb.MethodDescripto
 		p("")
 
 		body = "bytes.NewReader(jsonReq)"
+		logBody = "jsonReq"
 		g.imports[pbinfo.ImportSpec{Path: "bytes"}] = true
 	}
 
@@ -957,21 +945,10 @@ func (g *generator) lroRESTCall(servName string, m *descriptorpb.MethodDescripto
 	p("  httpReq = httpReq.WithContext(ctx)")
 	p("  httpReq.Header = headers")
 	p("")
-	p("  httpRsp, err := c.httpClient.Do(httpReq)")
+	p("  buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, %s, %q)", logBody, m.GetName())
 	p("  if err != nil{")
 	p("   return err")
 	p("  }")
-	p("  defer httpRsp.Body.Close()")
-	p("")
-	p("  if err = googleapi.CheckResponse(httpRsp); err != nil {")
-	p("    return err")
-	p("  }")
-	p("")
-	p("  buf, err := io.ReadAll(httpRsp.Body)")
-	p("  if err != nil {")
-	p("    return err")
-	p("  }")
-	p("")
 	p("  if err := unm.Unmarshal(buf, resp); err != nil {")
 	p("    return err")
 	p("  }")
@@ -992,9 +969,7 @@ func (g *generator) lroRESTCall(servName string, m *descriptorpb.MethodDescripto
 	p("")
 
 	g.imports[pbinfo.ImportSpec{Path: "fmt"}] = true
-	g.imports[pbinfo.ImportSpec{Path: "io"}] = true
 	g.imports[pbinfo.ImportSpec{Path: "cloud.google.com/go/longrunning"}] = true
-	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/googleapi"}] = true
 	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/protobuf/encoding/protojson"}] = true
 
 	return nil
@@ -1023,7 +998,7 @@ func (g *generator) emptyUnaryRESTCall(servName string, m *descriptorpb.MethodDe
 	// TODO(dovs): handle deadlines
 	// TODO(dovs): handle call options
 
-	body := "nil"
+	body, logBody := "nil", "nil"
 	verb := strings.ToUpper(info.verb)
 
 	// Marshal body for HTTP methods that take a body.
@@ -1044,6 +1019,7 @@ func (g *generator) emptyUnaryRESTCall(servName string, m *descriptorpb.MethodDe
 		p("}")
 		p("")
 		body = "bytes.NewReader(jsonReq)"
+		logBody = "jsonReq"
 		g.imports[pbinfo.ImportSpec{Path: "bytes"}] = true
 		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/protobuf/encoding/protojson"}] = true
 	}
@@ -1063,19 +1039,11 @@ func (g *generator) emptyUnaryRESTCall(servName string, m *descriptorpb.MethodDe
 	p("  httpReq = httpReq.WithContext(ctx)")
 	p("  httpReq.Header = headers")
 	p("")
-	p("  httpRsp, err := c.httpClient.Do(httpReq)")
-	p("  if err != nil{")
-	p("   return err")
-	p("  }")
-	p("  defer httpRsp.Body.Close()")
-	p("")
-	p("  // Returns nil if there is no error, otherwise wraps")
-	p("  // the response code and body into a non-nil error")
-	p("  return googleapi.CheckResponse(httpRsp)")
+	p("  _, err = executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, %s, %q)", logBody, m.GetName())
+	p("  return err")
 	p("  }, opts...)")
 	p("}")
 
-	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/googleapi"}] = true
 	g.imports[inSpec] = true
 	return nil
 }
@@ -1117,7 +1085,7 @@ func (g *generator) unaryRESTCall(servName string, m *descriptorpb.MethodDescrip
 	// TODO(dovs): handle deadlines?
 	// TODO(dovs): handle calloptions
 
-	body := "nil"
+	body, logBody := "nil", "nil"
 	verb := strings.ToUpper(info.verb)
 
 	// Marshal body for HTTP methods that take a body.
@@ -1139,6 +1107,7 @@ func (g *generator) unaryRESTCall(servName string, m *descriptorpb.MethodDescrip
 		p("")
 
 		body = "bytes.NewReader(jsonReq)"
+		logBody = "jsonReq"
 		g.imports[pbinfo.ImportSpec{Path: "bytes"}] = true
 		g.imports[pbinfo.ImportSpec{Path: "google.golang.org/protobuf/encoding/protojson"}] = true
 
@@ -1166,20 +1135,10 @@ func (g *generator) unaryRESTCall(servName string, m *descriptorpb.MethodDescrip
 	p("  httpReq = httpReq.WithContext(ctx)")
 	p("  httpReq.Header = headers")
 	p("")
-	p("  httpRsp, err := c.httpClient.Do(httpReq)")
+	p("  buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, %s, %q)", logBody, m.GetName())
 	p("  if err != nil{")
 	p("   return err")
 	p("  }")
-	p("  defer httpRsp.Body.Close()")
-	p("")
-	p("  if err = googleapi.CheckResponse(httpRsp); err != nil {")
-	p("    return err")
-	p("  }")
-	p("")
-	p("  buf, err := io.ReadAll(httpRsp.Body)")
-	p("  if err != nil {")
-	p("    return err")
-	p("  }")
 	p("")
 	if isHTTPBodyMessage {
 		p("resp.Data = buf")
@@ -1207,8 +1166,6 @@ func (g *generator) unaryRESTCall(servName string, m *descriptorpb.MethodDescrip
 	p(ret)
 	p("}")
 
-	g.imports[pbinfo.ImportSpec{Path: "io"}] = true
-	g.imports[pbinfo.ImportSpec{Path: "google.golang.org/api/googleapi"}] = true
 	g.imports[inSpec] = true
 	g.imports[outSpec] = true
 	return nil
diff --git a/internal/gengapic/stream.go b/internal/gengapic/stream.go
index d8c7107..cba474a 100644
--- a/internal/gengapic/stream.go
+++ b/internal/gengapic/stream.go
@@ -43,7 +43,9 @@ func (g *generator) noRequestStreamCall(servName string, s *descriptorpb.Service
 
 	p("  err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {")
 	p("    var err error")
+	p(`    c.logger.DebugContext(ctx, "api streaming client request", "serviceName", serviceName, "rpcName", %q)`, m.GetName())
 	p("    resp, err = c.%s.%s(ctx, settings.GRPC...)", grpcClientField(servName), m.GetName())
+	p(`    c.logger.DebugContext(ctx, "api streaming client response", "serviceName", serviceName, "rpcName", %q)`, m.GetName())
 	p("    return err")
 	p("  }, opts...)")
 	p("  if err != nil {")
@@ -83,7 +85,9 @@ func (g *generator) serverStreamCall(servName string, s *descriptorpb.ServiceDes
 	p("  var resp %s", retTyp)
 	p("err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {")
 	p("  var err error")
-	p("  resp, err = %s", g.grpcStubCall(m))
+	p(`  c.logger.DebugContext(ctx, "api streaming client request", "serviceName", serviceName, "rpcName", %q)`, m.GetName())
+	p("  resp, err = c.%s.%s(ctx, req, settings.GRPC...)", grpcClientField(servName), m.GetName())
+	p(`  c.logger.DebugContext(ctx, "api streaming client response", "serviceName", serviceName, "rpcName", %q)`, m.GetName())
 	p("  return err")
 	p("}, opts...)")
 	p("if err != nil {")


Test patch:
diff --git a/internal/gengapic/client_init_test.go b/internal/gengapic/client_init_test.go
index 74209f2..ce097de 100644
--- a/internal/gengapic/client_init_test.go
+++ b/internal/gengapic/client_init_test.go
@@ -414,6 +414,7 @@ func TestClientInit(t *testing.T) {
 				{Name: "iampb", Path: "cloud.google.com/go/iam/apiv1/iampb"}:                       true,
 				{Name: "locationpb", Path: "google.golang.org/genproto/googleapis/cloud/location"}: true,
 				{Name: "mypackagepb", Path: "github.com/googleapis/mypackage"}:                     true,
+				{Path: "log/slog"}: true,
 			},
 			wantNumSnps: 6,
 		},
@@ -436,6 +437,7 @@ func TestClientInit(t *testing.T) {
 				{Name: "iampb", Path: "cloud.google.com/go/iam/apiv1/iampb"}:                       true,
 				{Name: "locationpb", Path: "google.golang.org/genproto/googleapis/cloud/location"}: true,
 				{Name: "mypackagepb", Path: "github.com/googleapis/mypackage"}:                     true,
+				{Path: "log/slog"}:                                                                 true,
 			},
 			wantNumSnps: 6,
 		},
@@ -453,6 +455,7 @@ func TestClientInit(t *testing.T) {
 				{Name: "gtransport", Path: "google.golang.org/api/transport/grpc"}:    true,
 				{Name: "mypackagepb", Path: "github.com/googleapis/mypackage"}:        true,
 				{Name: "httptransport", Path: "google.golang.org/api/transport/http"}: true,
+				{Path: "log/slog"}:                                                    true,
 			},
 			wantNumSnps: 1,
 		},
@@ -472,6 +475,7 @@ func TestClientInit(t *testing.T) {
 				{Path: "context"}:                      true,
 				{Path: "google.golang.org/api/option"}: true,
 				{Path: "google.golang.org/grpc"}:       true,
+				{Path: "log/slog"}:                     true,
 			},
 			wantNumSnps: 6,
 		},
@@ -489,6 +493,7 @@ func TestClientInit(t *testing.T) {
 				{Path: "google.golang.org/api/option/internaloption"}: true,
 				{Path: "google.golang.org/grpc"}:                      true,
 				{Path: "net/http"}:                                    true,
+				{Path: "log/slog"}:                                    true,
 			},
 			wantNumSnps: 1,
 		},
@@ -506,6 +511,7 @@ func TestClientInit(t *testing.T) {
 				{Path: "net/http"}:                                                    true,
 				{Name: "httptransport", Path: "google.golang.org/api/transport/http"}: true,
 				{Name: "mypackagepb", Path: "github.com/googleapis/mypackage"}:        true,
+				{Path: "log/slog"}:                                                    true,
 			},
 			wantNumSnps: 1,
 			setExt: func() (protoreflect.ExtensionType, interface{}) {
diff --git a/internal/gengapic/gengapic_test.go b/internal/gengapic/gengapic_test.go
index 78c3577..5786c04 100644
--- a/internal/gengapic/gengapic_test.go
+++ b/internal/gengapic/gengapic_test.go
@@ -1128,12 +1128,12 @@ func TestGRPCStubCall(t *testing.T) {
 	}{
 		{
 			name: "foo.FooService.GetFoo",
-			want: "c.client.GetFoo(ctx, req, settings.GRPC...)",
+			want: `executeRPC(ctx, c.client.GetFoo, req, settings.GRPC, c.logger, "GetFoo")`,
 			in:   getFoo,
 		},
 		{
 			name: "foo.BarService.GetBar",
-			want: "c.barClient.GetBar(ctx, req, settings.GRPC...)",
+			want: `executeRPC(ctx, c.barClient.GetBar, req, settings.GRPC, c.logger, "GetBar")`,
 			in:   getBar,
 		},
 	} {
diff --git a/internal/gengapic/genrest_test.go b/internal/gengapic/genrest_test.go
index 0875694..9333b8d 100644
--- a/internal/gengapic/genrest_test.go
+++ b/internal/gengapic/genrest_test.go
@@ -756,10 +756,8 @@ func TestGenRestMethod(t *testing.T) {
 			options: &options{diregapic: true},
 			imports: map[pbinfo.ImportSpec]bool{
 				{Path: "google.golang.org/protobuf/encoding/protojson"}: true,
-				{Path: "io"}:      true,
 				{Path: "net/url"}: true,
 				{Path: "fmt"}:     true,
-				{Path: "google.golang.org/api/googleapi"}:                        true,
 				{Name: "foopb", Path: "google.golang.org/genproto/cloud/foo/v1"}: true,
 			},
 		},
@@ -768,10 +766,9 @@ func TestGenRestMethod(t *testing.T) {
 			method:  emptyRPC,
 			options: &options{},
 			imports: map[pbinfo.ImportSpec]bool{
-				{Path: "fmt"}:                             true,
-				{Path: "github.com/google/uuid"}:          true,
-				{Path: "google.golang.org/api/googleapi"}: true,
-				{Path: "net/url"}:                         true,
+				{Path: "fmt"}:                    true,
+				{Path: "github.com/google/uuid"}: true,
+				{Path: "net/url"}:                true,
 				{Name: "foopb", Path: "google.golang.org/genproto/cloud/foo/v1"}: true,
 			},
 		},
@@ -784,11 +781,9 @@ func TestGenRestMethod(t *testing.T) {
 				{Path: "fmt"}:                    true,
 				{Path: "github.com/google/uuid"}: true,
 				{Path: "google.golang.org/protobuf/encoding/protojson"}: true,
-				{Path: "io"}: true,
-				{Path: "google.golang.org/api/googleapi"}: true,
-				{Path: "net/url"}:                         true,
-				{Path: "regexp"}:                          true,
-				{Path: "strings"}:                         true,
+				{Path: "net/url"}: true,
+				{Path: "regexp"}:  true,
+				{Path: "strings"}: true,
 				{Name: "foopb", Path: "google.golang.org/genproto/cloud/foo/v1"}: true,
 			},
 		},
@@ -800,11 +795,9 @@ func TestGenRestMethod(t *testing.T) {
 				{Path: "math"}:    true,
 				{Path: "net/url"}: true,
 				{Path: "google.golang.org/protobuf/encoding/protojson"}: true,
-				{Path: "fmt"}: true,
-				{Path: "google.golang.org/api/googleapi"}:  true,
-				{Path: "google.golang.org/api/iterator"}:   true,
-				{Path: "google.golang.org/protobuf/proto"}: true,
-				{Path: "io"}: true,
+				{Path: "fmt"}:                                                    true,
+				{Path: "google.golang.org/api/iterator"}:                         true,
+				{Path: "google.golang.org/protobuf/proto"}:                       true,
 				{Name: "foopb", Path: "google.golang.org/genproto/cloud/foo/v1"}: true,
 			},
 		},
@@ -817,10 +810,9 @@ func TestGenRestMethod(t *testing.T) {
 				{Path: "context"}: true,
 				{Path: "errors"}:  true,
 				{Path: "fmt"}:     true,
-				{Path: "google.golang.org/api/googleapi"}:                        true,
-				{Path: "net/url"}:                                                true,
-				{Path: "regexp"}:                                                 true,
-				{Path: "strings"}:                                                true,
+				{Path: "net/url"}: true,
+				{Path: "regexp"}:  true,
+				{Path: "strings"}: true,
 				{Path: "google.golang.org/grpc/metadata"}:                        true,
 				{Path: "google.golang.org/protobuf/encoding/protojson"}:          true,
 				{Name: "foopb", Path: "google.golang.org/genproto/cloud/foo/v1"}: true,
@@ -844,9 +836,7 @@ func TestGenRestMethod(t *testing.T) {
 				{Path: "bytes"}: true,
 				{Path: "cloud.google.com/go/longrunning"}: true,
 				{Path: "fmt"}: true,
-				{Path: "google.golang.org/api/googleapi"}:               true,
 				{Path: "google.golang.org/protobuf/encoding/protojson"}: true,
-				{Path: "io"}:      true,
 				{Path: "net/url"}: true,
 				{Name: "longrunningpb", Path: "cloud.google.com/go/longrunning/autogen/longrunningpb"}: true,
 			},
@@ -859,11 +849,9 @@ func TestGenRestMethod(t *testing.T) {
 				{Path: "bytes"}: true,
 				{Path: "fmt"}:   true,
 				{Path: "google.golang.org/protobuf/encoding/protojson"}: true,
-				{Path: "io"}: true,
-				{Path: "google.golang.org/api/googleapi"}: true,
-				{Path: "net/url"}:                         true,
-				{Path: "regexp"}:                          true,
-				{Path: "strings"}:                         true,
+				{Path: "net/url"}: true,
+				{Path: "regexp"}:  true,
+				{Path: "strings"}: true,
 				{Name: "foopb", Path: "google.golang.org/genproto/cloud/foo/v1"}:                 true,
 				{Name: "httpbodypb", Path: "google.golang.org/genproto/googleapis/api/httpbody"}: true,
 			},
@@ -876,9 +864,7 @@ func TestGenRestMethod(t *testing.T) {
 				{Path: "bytes"}: true,
 				{Path: "fmt"}:   true,
 				{Path: "google.golang.org/protobuf/encoding/protojson"}: true,
-				{Path: "io"}: true,
-				{Path: "google.golang.org/api/googleapi"}:                        true,
-				{Path: "net/url"}:                                                true,
+				{Path: "net/url"}: true,
 				{Name: "foopb", Path: "google.golang.org/genproto/cloud/foo/v1"}: true,
 			},
 		},
diff --git a/internal/gengapic/testdata/custom_op_init.want b/internal/gengapic/testdata/custom_op_init.want
index 5ab35d2..1a190c4 100644
--- a/internal/gengapic/testdata/custom_op_init.want
+++ b/internal/gengapic/testdata/custom_op_init.want
@@ -63,6 +63,8 @@ type restClient struct {
 
 	// Points back to the CallOptions field of the containing Client
 	CallOptions **CallOptions
+
+	logger *slog.Logger
 }
 
 // NewRESTClient creates a new foo rest client.
@@ -80,6 +82,7 @@ func NewRESTClient(ctx context.Context, opts ...option.ClientOption) (*Client, e
 		endpoint: endpoint,
 		httpClient: httpClient,
 		CallOptions: &callOpts,
+		logger: internaloption.GetLogger(opts),
 	}
 	c.setGoogleClientInfo()
 
diff --git a/internal/gengapic/testdata/deprecated_client_init.want b/internal/gengapic/testdata/deprecated_client_init.want
index ff8f940..c45be8d 100644
--- a/internal/gengapic/testdata/deprecated_client_init.want
+++ b/internal/gengapic/testdata/deprecated_client_init.want
@@ -64,6 +64,8 @@ type gRPCClient struct {
 
 	// The x-goog-* metadata to be sent with each request.
 	xGoogHeaders []string
+
+	logger *slog.Logger
 }
 
 // NewClient creates a new foo client based on gRPC.
@@ -92,6 +94,7 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error
 		connPool:    connPool,
 		client: mypackagepb.NewFooClient(connPool),
 		CallOptions: &client.CallOptions,
+		logger: internaloption.GetLogger(opts),
 
 	}
 	c.setGoogleClientInfo()
@@ -139,6 +142,8 @@ type restClient struct {
 
 	// Points back to the CallOptions field of the containing Client
 	CallOptions **CallOptions
+
+	logger *slog.Logger
 }
 
 // NewRESTClient creates a new foo rest client.
@@ -158,6 +163,7 @@ func NewRESTClient(ctx context.Context, opts ...option.ClientOption) (*Client, e
 		endpoint: endpoint,
 		httpClient: httpClient,
 		CallOptions: &callOpts,
+		logger: internaloption.GetLogger(opts),
 	}
 	c.setGoogleClientInfo()
 
diff --git a/internal/gengapic/testdata/empty_client_init.want b/internal/gengapic/testdata/empty_client_init.want
index 1bafa25..1fec834 100644
--- a/internal/gengapic/testdata/empty_client_init.want
+++ b/internal/gengapic/testdata/empty_client_init.want
@@ -62,6 +62,8 @@ type gRPCClient struct {
 
 	// The x-goog-* metadata to be sent with each request.
 	xGoogHeaders []string
+
+	logger *slog.Logger
 }
 
 // NewClient creates a new foo client based on gRPC.
@@ -88,6 +90,7 @@ func NewClient(ctx context.Context, opts ...option.ClientOption) (*Client, error
 		connPool:    connPool,
 		client: mypackagepb.NewFooClient(connPool),
 		CallOptions: &client.CallOptions,
+		logger: internaloption.GetLogger(opts),
 
 	}
 	c.setGoogleClientInfo()
@@ -136,6 +139,8 @@ type restClient struct {
 
 	// Points back to the CallOptions field of the containing Client
 	CallOptions **CallOptions
+
+	logger *slog.Logger
 }
 
 // NewRESTClient creates a new foo rest client.
@@ -153,6 +158,7 @@ func NewRESTClient(ctx context.Context, opts ...option.ClientOption) (*Client, e
 		endpoint: endpoint,
 		httpClient: httpClient,
 		CallOptions: &callOpts,
+		logger: internaloption.GetLogger(opts),
 	}
 	c.setGoogleClientInfo()
 
diff --git a/internal/gengapic/testdata/foo_client_init.want b/internal/gengapic/testdata/foo_client_init.want
index e32c9ea..9a5f5e7 100644
--- a/internal/gengapic/testdata/foo_client_init.want
+++ b/internal/gengapic/testdata/foo_client_init.want
@@ -91,6 +91,8 @@ type fooGRPCClient struct {
 
 	// The x-goog-* metadata to be sent with each request.
 	xGoogHeaders []string
+
+	logger *slog.Logger
 }
 
 // NewFooClient creates a new foo client based on gRPC.
@@ -117,6 +119,7 @@ func NewFooClient(ctx context.Context, opts ...option.ClientOption) (*FooClient,
 		connPool:    connPool,
 		fooClient: mypackagepb.NewFooClient(connPool),
 		CallOptions: &client.CallOptions,
+		logger: internaloption.GetLogger(opts),
 		iamPolicyClient: iampb.NewIAMPolicyClient(connPool),
 		locationsClient: locationpb.NewLocationsClient(connPool),
 
diff --git a/internal/gengapic/testdata/foo_rest_client_init.want b/internal/gengapic/testdata/foo_rest_client_init.want
index c24c9ef..e69085c 100644
--- a/internal/gengapic/testdata/foo_rest_client_init.want
+++ b/internal/gengapic/testdata/foo_rest_client_init.want
@@ -85,6 +85,8 @@ type fooRESTClient struct {
 
 	// Points back to the CallOptions field of the containing FooClient
 	CallOptions **FooCallOptions
+
+	logger *slog.Logger
 }
 
 // NewFooRESTClient creates a new foo rest client.
@@ -102,6 +104,7 @@ func NewFooRESTClient(ctx context.Context, opts ...option.ClientOption) (*FooCli
 		endpoint: endpoint,
 		httpClient: httpClient,
 		CallOptions: &callOpts,
+		logger: internaloption.GetLogger(opts),
 	}
 	c.setGoogleClientInfo()
 
diff --git a/internal/gengapic/testdata/helpers_default_scope.want b/internal/gengapic/testdata/helpers_default_scope.want
index 1cc4b5f..9fdce53 100644
--- a/internal/gengapic/testdata/helpers_default_scope.want
+++ b/internal/gengapic/testdata/helpers_default_scope.want
@@ -1,3 +1,5 @@
+const serviceName = "secretmanager.googleapis.com"
+
 // For more information on implementing a client constructor hook, see
 // https://github.com/googleapis/google-cloud-go/wiki/Customizing-constructors.
 type clientHookParams struct{}
@@ -18,3 +20,46 @@ func DefaultAuthScopes() []string {
 		"https://www.googleapis.com/auth/cloud-platform",
 	}
 }
+
+func executeHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger,	body []byte, rpc string) ([]byte, error) {
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))
+	resp, err := client.Do(req)
+	if err != nil{
+		return nil, err
+	}
+	defer resp.Body.Close()
+	buf, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, buf))
+	if err = googleapi.CheckResponse(resp); err != nil {
+		return nil, err
+	}
+	return buf, nil
+}
+
+func executeStreamingHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger, body []byte, rpc string) (*http.Response, error) {
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))
+	resp, err := client.Do(req)
+	if err != nil{
+		return nil, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, nil))
+	if err = googleapi.CheckResponse(resp); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func executeRPC[I proto.Message, O proto.Message](ctx context.Context, fn func(context.Context, I, ...grpc.CallOption) (O, error), req I, opts []grpc.CallOption, logger *slog.Logger, rpc string) (O, error) {
+	var zero O
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", grpclog.ProtoMessageRequest(ctx, req))
+	resp, err := fn(ctx, req, opts...)
+	if err != nil {
+		return zero, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", grpclog.ProtoMessageResponse(resp))
+	return resp, err
+}
+
diff --git a/internal/gengapic/testdata/helpers_multiple_scopes.want b/internal/gengapic/testdata/helpers_multiple_scopes.want
index 9b2583d..2cf2254 100644
--- a/internal/gengapic/testdata/helpers_multiple_scopes.want
+++ b/internal/gengapic/testdata/helpers_multiple_scopes.want
@@ -1,3 +1,5 @@
+const serviceName = "secretmanager.googleapis.com"
+
 // For more information on implementing a client constructor hook, see
 // https://github.com/googleapis/google-cloud-go/wiki/Customizing-constructors.
 type clientHookParams struct{}
@@ -20,3 +22,46 @@ func DefaultAuthScopes() []string {
 		"scope-c",
 	}
 }
+
+func executeHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger,	body []byte, rpc string) ([]byte, error) {
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))
+	resp, err := client.Do(req)
+	if err != nil{
+		return nil, err
+	}
+	defer resp.Body.Close()
+	buf, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, buf))
+	if err = googleapi.CheckResponse(resp); err != nil {
+		return nil, err
+	}
+	return buf, nil
+}
+
+func executeStreamingHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger, body []byte, rpc string) (*http.Response, error) {
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))
+	resp, err := client.Do(req)
+	if err != nil{
+		return nil, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, nil))
+	if err = googleapi.CheckResponse(resp); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func executeRPC[I proto.Message, O proto.Message](ctx context.Context, fn func(context.Context, I, ...grpc.CallOption) (O, error), req I, opts []grpc.CallOption, logger *slog.Logger, rpc string) (O, error) {
+	var zero O
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", grpclog.ProtoMessageRequest(ctx, req))
+	resp, err := fn(ctx, req, opts...)
+	if err != nil {
+		return zero, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", grpclog.ProtoMessageResponse(resp))
+	return resp, err
+}
+
diff --git a/internal/gengapic/testdata/helpers_no_scopes.want b/internal/gengapic/testdata/helpers_no_scopes.want
index d72cc5b..b596b1e 100644
--- a/internal/gengapic/testdata/helpers_no_scopes.want
+++ b/internal/gengapic/testdata/helpers_no_scopes.want
@@ -1,3 +1,5 @@
+const serviceName = "secretmanager.googleapis.com"
+
 // For more information on implementing a client constructor hook, see
 // https://github.com/googleapis/google-cloud-go/wiki/Customizing-constructors.
 type clientHookParams struct{}
@@ -17,3 +19,46 @@ func DefaultAuthScopes() []string {
 	return []string{
 	}
 }
+
+func executeHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger,	body []byte, rpc string) ([]byte, error) {
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))
+	resp, err := client.Do(req)
+	if err != nil{
+		return nil, err
+	}
+	defer resp.Body.Close()
+	buf, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, buf))
+	if err = googleapi.CheckResponse(resp); err != nil {
+		return nil, err
+	}
+	return buf, nil
+}
+
+func executeStreamingHTTPRequest(ctx context.Context, client *http.Client, req *http.Request, logger *slog.Logger, body []byte, rpc string) (*http.Response, error) {
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", internallog.HTTPRequest(req, body))
+	resp, err := client.Do(req)
+	if err != nil{
+		return nil, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", internallog.HTTPResponse(resp, nil))
+	if err = googleapi.CheckResponse(resp); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func executeRPC[I proto.Message, O proto.Message](ctx context.Context, fn func(context.Context, I, ...grpc.CallOption) (O, error), req I, opts []grpc.CallOption, logger *slog.Logger, rpc string) (O, error) {
+	var zero O
+	logger.DebugContext(ctx, "api request", "serviceName", serviceName, "rpcName", rpc, "request", grpclog.ProtoMessageRequest(ctx, req))
+	resp, err := fn(ctx, req, opts...)
+	if err != nil {
+		return zero, err
+	}
+	logger.DebugContext(ctx, "api response", "serviceName", serviceName, "rpcName", rpc, "response", grpclog.ProtoMessageResponse(resp))
+	return resp, err
+}
+
diff --git a/internal/gengapic/testdata/lro_client_init.want b/internal/gengapic/testdata/lro_client_init.want
index feb4dc2..619eae0 100644
--- a/internal/gengapic/testdata/lro_client_init.want
+++ b/internal/gengapic/testdata/lro_client_init.want
@@ -106,6 +106,8 @@ type fooGRPCClient struct {
 
 	// The x-goog-* metadata to be sent with each request.
 	xGoogHeaders []string
+
+	logger *slog.Logger
 }
 
 // NewFooClient creates a new foo client based on gRPC.
@@ -132,6 +134,7 @@ func NewFooClient(ctx context.Context, opts ...option.ClientOption) (*FooClient,
 		connPool:    connPool,
 		fooClient: mypackagepb.NewFooClient(connPool),
 		CallOptions: &client.CallOptions,
+		logger: internaloption.GetLogger(opts),
 		operationsClient: longrunningpb.NewOperationsClient(connPool),
 
 	}
diff --git a/internal/gengapic/testdata/method_BidiThings.want b/internal/gengapic/testdata/method_BidiThings.want
index 7335ad1..de93bbe 100644
--- a/internal/gengapic/testdata/method_BidiThings.want
+++ b/internal/gengapic/testdata/method_BidiThings.want
@@ -4,7 +4,9 @@ func (c *fooGRPCClient) BidiThings(ctx context.Context, opts ...gax.CallOption)
 	opts = append((*c.CallOptions).BidiThings[0:len((*c.CallOptions).BidiThings):len((*c.CallOptions).BidiThings)], opts...)
 	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 		var err error
+		c.logger.DebugContext(ctx, "api streaming client request", "serviceName", serviceName, "rpcName", "BidiThings")
 		resp, err = c.fooClient.BidiThings(ctx, settings.GRPC...)
+		c.logger.DebugContext(ctx, "api streaming client response", "serviceName", serviceName, "rpcName", "BidiThings")
 		return err
 	}, opts...)
 	if err != nil {
diff --git a/internal/gengapic/testdata/method_ClientThings.want b/internal/gengapic/testdata/method_ClientThings.want
index 51a3d2a..ac016d4 100644
--- a/internal/gengapic/testdata/method_ClientThings.want
+++ b/internal/gengapic/testdata/method_ClientThings.want
@@ -4,7 +4,9 @@ func (c *fooGRPCClient) ClientThings(ctx context.Context, opts ...gax.CallOption
 	opts = append((*c.CallOptions).ClientThings[0:len((*c.CallOptions).ClientThings):len((*c.CallOptions).ClientThings)], opts...)
 	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 		var err error
+		c.logger.DebugContext(ctx, "api streaming client request", "serviceName", serviceName, "rpcName", "ClientThings")
 		resp, err = c.fooClient.ClientThings(ctx, settings.GRPC...)
+		c.logger.DebugContext(ctx, "api streaming client response", "serviceName", serviceName, "rpcName", "ClientThings")
 		return err
 	}, opts...)
 	if err != nil {
diff --git a/internal/gengapic/testdata/method_EmptyLRO.want b/internal/gengapic/testdata/method_EmptyLRO.want
index e077532..bf16102 100644
--- a/internal/gengapic/testdata/method_EmptyLRO.want
+++ b/internal/gengapic/testdata/method_EmptyLRO.want
@@ -4,7 +4,7 @@ func (c *fooGRPCClient) EmptyLRO(ctx context.Context, req *mypackagepb.InputType
 	var resp *longrunningpb.Operation
 	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 		var err error
-		resp, err = c.fooClient.EmptyLRO(ctx, req, settings.GRPC...)
+		resp, err = executeRPC(ctx, c.fooClient.EmptyLRO, req, settings.GRPC, c.logger, "EmptyLRO")
 		return err
 	}, opts...)
 	if err != nil {
diff --git a/internal/gengapic/testdata/method_GetAnotherThing.want b/internal/gengapic/testdata/method_GetAnotherThing.want
index c51f14b..1849b02 100644
--- a/internal/gengapic/testdata/method_GetAnotherThing.want
+++ b/internal/gengapic/testdata/method_GetAnotherThing.want
@@ -34,7 +34,7 @@ func (c *fooGRPCClient) GetAnotherThing(ctx context.Context, req *mypackagepb.In
 	var resp *mypackagepb.OutputType
 	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 		var err error
-		resp, err = c.fooClient.GetAnotherThing(ctx, req, settings.GRPC...)
+		resp, err = executeRPC(ctx, c.fooClient.GetAnotherThing, req, settings.GRPC, c.logger, "GetAnotherThing")
 		return err
 	}, opts...)
 	if err != nil {
diff --git a/internal/gengapic/testdata/method_GetEmptyThing.want b/internal/gengapic/testdata/method_GetEmptyThing.want
index eaf6c42..ee58f16 100644
--- a/internal/gengapic/testdata/method_GetEmptyThing.want
+++ b/internal/gengapic/testdata/method_GetEmptyThing.want
@@ -9,7 +9,7 @@ func (c *fooGRPCClient) GetEmptyThing(ctx context.Context, req *mypackagepb.Inpu
 	opts = append((*c.CallOptions).GetEmptyThing[0:len((*c.CallOptions).GetEmptyThing):len((*c.CallOptions).GetEmptyThing)], opts...)
 	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 		var err error
-		_, err = c.fooClient.GetEmptyThing(ctx, req, settings.GRPC...)
+		_, err = executeRPC(ctx, c.fooClient.GetEmptyThing, req, settings.GRPC, c.logger, "GetEmptyThing")
 		return err
 	}, opts...)
 	return err
diff --git a/internal/gengapic/testdata/method_GetManyOtherThings.want b/internal/gengapic/testdata/method_GetManyOtherThings.want
index 0c402bd..fd80fe5 100644
--- a/internal/gengapic/testdata/method_GetManyOtherThings.want
+++ b/internal/gengapic/testdata/method_GetManyOtherThings.want
@@ -15,7 +15,7 @@ func (c *fooGRPCClient) GetManyOtherThings(ctx context.Context, req *mypackagepb
 		}
 		err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 			var err error
-			resp, err = c.fooClient.GetManyOtherThings(ctx, req, settings.GRPC...)
+			resp, err = executeRPC(ctx, c.fooClient.GetManyOtherThings, req, settings.GRPC, c.logger, "GetManyOtherThings")
 			return err
 		}, opts...)
 		if err != nil {
diff --git a/internal/gengapic/testdata/method_GetManyThings.want b/internal/gengapic/testdata/method_GetManyThings.want
index 776da67..1e8581e 100644
--- a/internal/gengapic/testdata/method_GetManyThings.want
+++ b/internal/gengapic/testdata/method_GetManyThings.want
@@ -18,7 +18,7 @@ func (c *fooGRPCClient) GetManyThings(ctx context.Context, req *mypackagepb.Page
 		}
 		err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 			var err error
-			resp, err = c.fooClient.GetManyThings(ctx, req, settings.GRPC...)
+			resp, err = executeRPC(ctx, c.fooClient.GetManyThings, req, settings.GRPC, c.logger, "GetManyThings")
 			return err
 		}, opts...)
 		if err != nil {
diff --git a/internal/gengapic/testdata/method_GetManyThingsOptional.want b/internal/gengapic/testdata/method_GetManyThingsOptional.want
index 7d2761f..5400460 100644
--- a/internal/gengapic/testdata/method_GetManyThingsOptional.want
+++ b/internal/gengapic/testdata/method_GetManyThingsOptional.want
@@ -18,7 +18,7 @@ func (c *fooGRPCClient) GetManyThingsOptional(ctx context.Context, req *mypackag
 		}
 		err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 			var err error
-			resp, err = c.fooClient.GetManyThingsOptional(ctx, req, settings.GRPC...)
+			resp, err = executeRPC(ctx, c.fooClient.GetManyThingsOptional, req, settings.GRPC, c.logger, "GetManyThingsOptional")
 			return err
 		}, opts...)
 		if err != nil {
diff --git a/internal/gengapic/testdata/method_GetOneThing.want b/internal/gengapic/testdata/method_GetOneThing.want
index 54ba5d7..2bf6a03 100644
--- a/internal/gengapic/testdata/method_GetOneThing.want
+++ b/internal/gengapic/testdata/method_GetOneThing.want
@@ -13,7 +13,7 @@ func (c *fooGRPCClient) GetOneThing(ctx context.Context, req *mypackagepb.InputT
 	var resp *mypackagepb.OutputType
 	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 		var err error
-		resp, err = c.fooClient.GetOneThing(ctx, req, settings.GRPC...)
+		resp, err = executeRPC(ctx, c.fooClient.GetOneThing, req, settings.GRPC, c.logger, "GetOneThing")
 		return err
 	}, opts...)
 	if err != nil {
diff --git a/internal/gengapic/testdata/method_RespLRO.want b/internal/gengapic/testdata/method_RespLRO.want
index 558c523..8ca199e 100644
--- a/internal/gengapic/testdata/method_RespLRO.want
+++ b/internal/gengapic/testdata/method_RespLRO.want
@@ -4,7 +4,7 @@ func (c *fooGRPCClient) RespLRO(ctx context.Context, req *mypackagepb.InputType,
 	var resp *longrunningpb.Operation
 	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 		var err error
-		resp, err = c.fooClient.RespLRO(ctx, req, settings.GRPC...)
+		resp, err = executeRPC(ctx, c.fooClient.RespLRO, req, settings.GRPC, c.logger, "RespLRO")
 		return err
 	}, opts...)
 	if err != nil {
diff --git a/internal/gengapic/testdata/method_ServerThings.want b/internal/gengapic/testdata/method_ServerThings.want
index 7551461..c177fcc 100644
--- a/internal/gengapic/testdata/method_ServerThings.want
+++ b/internal/gengapic/testdata/method_ServerThings.want
@@ -7,7 +7,9 @@ func (c *fooGRPCClient) ServerThings(ctx context.Context, req *mypackagepb.Input
 	var resp mypackagepb.Foo_ServerThingsClient
 	err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
 		var err error
+		c.logger.DebugContext(ctx, "api streaming client request", "serviceName", serviceName, "rpcName", "ServerThings")
 		resp, err = c.fooClient.ServerThings(ctx, req, settings.GRPC...)
+		c.logger.DebugContext(ctx, "api streaming client response", "serviceName", serviceName, "rpcName", "ServerThings")
 		return err
 	}, opts...)
 	if err != nil {
diff --git a/internal/gengapic/testdata/rest_CustomOp.want b/internal/gengapic/testdata/rest_CustomOp.want
index faa858f..47e7439 100644
--- a/internal/gengapic/testdata/rest_CustomOp.want
+++ b/internal/gengapic/testdata/rest_CustomOp.want
@@ -33,20 +33,10 @@ func (c *fooRESTClient) CustomOp(ctx context.Context, req *foopb.Foo, opts ...ga
 		httpReq = httpReq.WithContext(ctx)
 		httpReq.Header = headers
 
-		httpRsp, err := c.httpClient.Do(httpReq)
+		buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, nil, "CustomOp")
 		if err != nil{
 			return err
 		}
-		defer httpRsp.Body.Close()
-
-		if err = googleapi.CheckResponse(httpRsp); err != nil {
-			return err
-		}
-
-		buf, err := io.ReadAll(httpRsp.Body)
-		if err != nil {
-			return err
-		}
 
 		if err := unm.Unmarshal(buf, resp); err != nil {
 			return err
diff --git a/internal/gengapic/testdata/rest_EmptyRPC.want b/internal/gengapic/testdata/rest_EmptyRPC.want
index 1cdcc02..1ca911d 100644
--- a/internal/gengapic/testdata/rest_EmptyRPC.want
+++ b/internal/gengapic/testdata/rest_EmptyRPC.want
@@ -33,14 +33,7 @@ func (c *fooRESTClient) EmptyRPC(ctx context.Context, req *foopb.Foo, opts ...ga
 		httpReq = httpReq.WithContext(ctx)
 		httpReq.Header = headers
 
-		httpRsp, err := c.httpClient.Do(httpReq)
-		if err != nil{
-			return err
-		}
-		defer httpRsp.Body.Close()
-
-		// Returns nil if there is no error, otherwise wraps
-		// the response code and body into a non-nil error
-		return googleapi.CheckResponse(httpRsp)
+		_, err = executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, nil, "EmptyRPC")
+		return err
 	}, opts...)
 }
diff --git a/internal/gengapic/testdata/rest_HttpBodyRPC.want b/internal/gengapic/testdata/rest_HttpBodyRPC.want
index 1e9e696..1d7d5d5 100644
--- a/internal/gengapic/testdata/rest_HttpBodyRPC.want
+++ b/internal/gengapic/testdata/rest_HttpBodyRPC.want
@@ -39,20 +39,10 @@ func (c *fooRESTClient) HttpBodyRPC(ctx context.Context, req *foopb.Foo, opts ..
 		httpReq = httpReq.WithContext(ctx)
 		httpReq.Header = headers
 
-		httpRsp, err := c.httpClient.Do(httpReq)
+		buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, jsonReq, "HttpBodyRPC")
 		if err != nil{
 			return err
 		}
-		defer httpRsp.Body.Close()
-
-		if err = googleapi.CheckResponse(httpRsp); err != nil {
-			return err
-		}
-
-		buf, err := io.ReadAll(httpRsp.Body)
-		if err != nil {
-			return err
-		}
 
 		resp.Data = buf
 		if headers := httpRsp.Header; len(headers["Content-Type"]) > 0 {
diff --git a/internal/gengapic/testdata/rest_LongrunningRPC.want b/internal/gengapic/testdata/rest_LongrunningRPC.want
index 504a89a..3cd9638 100644
--- a/internal/gengapic/testdata/rest_LongrunningRPC.want
+++ b/internal/gengapic/testdata/rest_LongrunningRPC.want
@@ -27,21 +27,10 @@ func (c *fooRESTClient) LongrunningRPC(ctx context.Context, req *foopb.Foo, opts
 		httpReq = httpReq.WithContext(ctx)
 		httpReq.Header = headers
 
-		httpRsp, err := c.httpClient.Do(httpReq)
+		buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, jsonReq, "LongrunningRPC")
 		if err != nil{
 			return err
 		}
-		defer httpRsp.Body.Close()
-
-		if err = googleapi.CheckResponse(httpRsp); err != nil {
-			return err
-		}
-
-		buf, err := io.ReadAll(httpRsp.Body)
-		if err != nil {
-			return err
-		}
-
 		if err := unm.Unmarshal(buf, resp); err != nil {
 			return err
 		}
diff --git a/internal/gengapic/testdata/rest_PagingRPC.want b/internal/gengapic/testdata/rest_PagingRPC.want
index f1d4e3b..c60231f 100644
--- a/internal/gengapic/testdata/rest_PagingRPC.want
+++ b/internal/gengapic/testdata/rest_PagingRPC.want
@@ -41,21 +41,10 @@ func (c *fooRESTClient) PagingRPC(ctx context.Context, req *foopb.PagedFooReques
 			}
 			httpReq.Header = headers
 
-			httpRsp, err := c.httpClient.Do(httpReq)
+			buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, nil, "PagingRPC")
 			if err != nil{
 				return err
 			}
-			defer httpRsp.Body.Close()
-
-			if err = googleapi.CheckResponse(httpRsp); err != nil {
-				return err
-			}
-
-			buf, err := io.ReadAll(httpRsp.Body)
-			if err != nil {
-				return err
-			}
-
 			if err := unm.Unmarshal(buf, resp); err != nil {
 				return err
 			}
diff --git a/internal/gengapic/testdata/rest_ServerStreamRPC.want b/internal/gengapic/testdata/rest_ServerStreamRPC.want
index 0511d21..a303fd8 100644
--- a/internal/gengapic/testdata/rest_ServerStreamRPC.want
+++ b/internal/gengapic/testdata/rest_ServerStreamRPC.want
@@ -38,15 +38,11 @@ func (c *fooRESTClient) ServerStreamRPC(ctx context.Context, req *foopb.Foo, opt
 		httpReq = httpReq.WithContext(ctx)
 		httpReq.Header = headers
 
-		httpRsp, err := c.httpClient.Do(httpReq)
+		httpRsp, err := executeStreamingHTTPRequest(ctx, c.httpClient, httpReq, c.logger, jsonReq, "ServerStreamRPC")
 		if err != nil{
 			return err
 		}
 
-		if err = googleapi.CheckResponse(httpRsp); err != nil {
-			return err
-		}
-
 		streamClient = &serverStreamRPCRESTClient{
 			ctx: ctx,
 			md: metadata.MD(httpRsp.Header),
diff --git a/internal/gengapic/testdata/rest_UnaryRPC.want b/internal/gengapic/testdata/rest_UnaryRPC.want
index 473af58..dbf6297 100644
--- a/internal/gengapic/testdata/rest_UnaryRPC.want
+++ b/internal/gengapic/testdata/rest_UnaryRPC.want
@@ -48,20 +48,10 @@ func (c *fooRESTClient) UnaryRPC(ctx context.Context, req *foopb.Foo, opts ...ga
 		httpReq = httpReq.WithContext(ctx)
 		httpReq.Header = headers
 
-		httpRsp, err := c.httpClient.Do(httpReq)
+		buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, jsonReq, "UnaryRPC")
 		if err != nil{
 			return err
 		}
-		defer httpRsp.Body.Close()
-
-		if err = googleapi.CheckResponse(httpRsp); err != nil {
-			return err
-		}
-
-		buf, err := io.ReadAll(httpRsp.Body)
-		if err != nil {
-			return err
-		}
 
 		if err := unm.Unmarshal(buf, resp); err != nil {
 			return err
diff --git a/internal/gengapic/testdata/rest_UpdateRPC.want b/internal/gengapic/testdata/rest_UpdateRPC.want
index 2c156d7..694ec10 100644
--- a/internal/gengapic/testdata/rest_UpdateRPC.want
+++ b/internal/gengapic/testdata/rest_UpdateRPC.want
@@ -53,20 +53,10 @@ func (c *fooRESTClient) UpdateRPC(ctx context.Context, req *foopb.UpdateRequest,
 		httpReq = httpReq.WithContext(ctx)
 		httpReq.Header = headers
 
-		httpRsp, err := c.httpClient.Do(httpReq)
+		buf, err := executeHTTPRequest(ctx, c.httpClient, httpReq, c.logger, jsonReq, "UpdateRPC")
 		if err != nil{
 			return err
 		}
-		defer httpRsp.Body.Close()
-
-		if err = googleapi.CheckResponse(httpRsp); err != nil {
-			return err
-		}
-
-		buf, err := io.ReadAll(httpRsp.Body)
-		if err != nil {
-			return err
-		}
 
 		if err := unm.Unmarshal(buf, resp); err != nil {
 			return err