Skip to content

Commit

Permalink
Merge pull request #26 from coolwednesday/main
Browse files Browse the repository at this point in the history
"gofr wrap grpc client" to add tracing in inter-service gRPC Calls
  • Loading branch information
Umang01-hash authored Jan 20, 2025
2 parents 45fbbd3 + b3d61b0 commit e092d61
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 73 deletions.
4 changes: 3 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ func main() {

cli.SubCommand("migrate create", migration.Migrate)

cli.SubCommand("wrap grpc", wrap.GenerateWrapper)
cli.SubCommand("wrap grpc server", wrap.BuildGRPCGoFrServer)

cli.SubCommand("wrap grpc client", wrap.BuildGRPCGoFrClient)

cli.Run()
}
148 changes: 77 additions & 71 deletions wrap/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ import (
"gofr.dev/pkg/gofr"
)

const filePerm = 0644
const (
filePerm = 0644
serverFileSuffix = "_server.go"
serverWrapperFileSuffix = "_gofr.go"
clientFileSuffix = "_client.go"
)

var (
ErrNoProtoFile = errors.New("proto file path is required")
ErrOpeningProtoFile = errors.New("error opening the proto file")
ErrFailedToParseProto = errors.New("failed to parse proto file")
ErrGeneratingWrapper = errors.New("error generating the wrapper code from the proto file")
ErrWritingWrapperFile = errors.New("error writing the generated wrapper to the file")
ErrGeneratingServerTemplate = errors.New("error generating the gRPC server file template")
ErrWritingServerTemplate = errors.New("error writing the generated server template to the file")
ErrNoProtoFile = errors.New("proto file path is required")
ErrOpeningProtoFile = errors.New("error opening the proto file")
ErrFailedToParseProto = errors.New("failed to parse proto file")
ErrGeneratingWrapper = errors.New("error while generating the code using proto file")
ErrWritingFile = errors.New("error writing the generated code to the file")
)

// ServiceMethod represents a method in a proto service.
Expand All @@ -47,7 +50,35 @@ type WrapperData struct {
Requests []string
}

func GenerateWrapper(ctx *gofr.Context) (any, error) {
type FileType struct {
FileSuffix string
CodeGenerator func(*gofr.Context, *WrapperData) string
}

// BuildGRPCGoFrClient generates gRPC client wrapper code based on a proto definition.
func BuildGRPCGoFrClient(ctx *gofr.Context) (any, error) {
gRPCClient := FileType{
FileSuffix: clientFileSuffix,
CodeGenerator: generateGoFrClient,
}

return generateWrapper(ctx, gRPCClient)
}

// BuildGRPCGoFrServer generates gRPC client and server code based on a proto definition.
func BuildGRPCGoFrServer(ctx *gofr.Context) (any, error) {
gRPCServer := []FileType{
{FileSuffix: serverWrapperFileSuffix, CodeGenerator: generateGoFrServerWrapper},
{FileSuffix: serverFileSuffix, CodeGenerator: generateGoFrServer},
}

return generateWrapper(ctx, gRPCServer...)
}

// generateWrapper executes the function for specified FileType to create GoFr integrated
// gRPC server/client files with the required services in proto file and
// specified suffix for every service specified in the proto file.
func generateWrapper(ctx *gofr.Context, options ...FileType) (any, error) {
protoPath := ctx.Param("proto")
if protoPath == "" {
return nil, ErrNoProtoFile
Expand All @@ -56,27 +87,20 @@ func GenerateWrapper(ctx *gofr.Context) (any, error) {
file, err := os.Open(protoPath)
if err != nil {
ctx.Errorf("Failed to open proto file: %v", err)

return nil, ErrOpeningProtoFile
}

defer file.Close()

parser := proto.NewParser(file)

definition, err := parser.Parse()
if err != nil {
ctx.Errorf("Failed to parse proto file: %v", err)

return nil, ErrFailedToParseProto
}

var (
// Extracting package and project path from go_package option.
projectPath, packageName = getPackageAndProject(definition, protoPath)
// Extract the services.
services = getServices(definition)
)
projectPath, packageName := getPackageAndProject(definition, protoPath)
services := getServices(definition)

for _, service := range services {
wrapperData := WrapperData{
Expand All @@ -86,84 +110,66 @@ func GenerateWrapper(ctx *gofr.Context) (any, error) {
Requests: uniqueRequestTypes(service.Methods),
}

generatedCode := generateWrapperCode(ctx, &wrapperData)
if generatedCode == "" {
return nil, ErrGeneratingWrapper
}

outputFilePath := path.Join(projectPath, fmt.Sprintf("%s_gofr.go", strings.ToLower(service.Name)))

err := os.WriteFile(outputFilePath, []byte(generatedCode), filePerm)
if err != nil {
ctx.Errorf("Failed to write file %s: %v", outputFilePath, err)

return nil, ErrWritingWrapperFile
}

fmt.Printf("Generated wrapper for service %s at %s\n", service.Name, outputFilePath)

generatedgRPCCode := generategRPCCode(ctx, &wrapperData)
if generatedgRPCCode == "" {
return nil, ErrGeneratingServerTemplate
}

outputFilePath = path.Join(projectPath, fmt.Sprintf("%s_server.go", strings.ToLower(service.Name)))
for _, option := range options {
generatedCode := option.CodeGenerator(ctx, &wrapperData)
if generatedCode == "" {
ctx.Errorf("Failed to generate code for service %s with file suffix %s", service.Name, option.FileSuffix)
return nil, ErrGeneratingWrapper
}

err = os.WriteFile(outputFilePath, []byte(generatedgRPCCode), filePerm)
if err != nil {
ctx.Errorf("Failed to write file %s: %v", outputFilePath, err)
// Generate output file path based on service name and file suffix.
outputFilePath := path.Join(projectPath, strings.ToLower(service.Name)+option.FileSuffix)
if writeErr := os.WriteFile(outputFilePath, []byte(generatedCode), filePerm); writeErr != nil {
ctx.Errorf("Failed to write file %s: %v", outputFilePath, writeErr)
return nil, ErrWritingFile
}

return nil, ErrWritingServerTemplate
fmt.Printf("Generated file for service %s at %s\n", service.Name, outputFilePath)
}

fmt.Printf("Generated server template for service %s at %s\n", service.Name, outputFilePath)
}

return "Successfully generated all wrappers for gRPC services", nil
return "Successfully generated all files for GoFr integrated gRPC servers/clients", nil
}

// Extract unique request types from methods.
func uniqueRequestTypes(methods []ServiceMethod) []string {
requests := make(map[string]bool)
req := make([]string, 0)

for _, method := range methods {
if !method.Streaming {
requests[method.Request] = true
}
}

for method := range requests {
req = append(req, method)
uniqueRequests := make([]string, 0, len(requests))
for request := range requests {
uniqueRequests = append(uniqueRequests, request)
}

return req
return uniqueRequests
}

// Generate wrapper code using the template.
func generateWrapperCode(ctx *gofr.Context, data *WrapperData) string {
var buf bytes.Buffer

tmplInstance := template.Must(template.New("wrapper").Parse(wrapperTemplate))

err := tmplInstance.Execute(&buf, data)
if err != nil {
ctx.Errorf("Template execution failed: %v", err)
// Generate GoFr server wrapper for gRPC using the wrapperTemplate.
func generateGoFrServerWrapper(ctx *gofr.Context, data *WrapperData) string {
return executeTemplate(ctx, data, wrapperTemplate)
}

return ""
}
// Generate GoFr gRPCHandler code using the serverTemplate.
func generateGoFrServer(ctx *gofr.Context, data *WrapperData) string {
return executeTemplate(ctx, data, serverTemplate)
}

return buf.String()
// Generate GoFr gRPC Client code using the clientTemplate.
func generateGoFrClient(ctx *gofr.Context, data *WrapperData) string {
return executeTemplate(ctx, data, clientTemplate)
}

// Generate wrapper code using the template.
func generategRPCCode(ctx *gofr.Context, data *WrapperData) string {
// Execute a template with data.
func executeTemplate(ctx *gofr.Context, data *WrapperData, tmpl string) string {
var buf bytes.Buffer

tmplInstance := template.Must(template.New("wrapper").Parse(serverTemplate))

err := tmplInstance.Execute(&buf, data)
if err != nil {
tmplInstance := template.Must(template.New("template").Parse(tmpl))
if err := tmplInstance.Execute(&buf, data); err != nil {
ctx.Errorf("Template execution failed: %v", err)
return ""
}
Expand All @@ -175,13 +181,12 @@ func getPackageAndProject(definition *proto.Proto, protoPath string) (projectPat
proto.Walk(definition,
proto.WithOption(func(opt *proto.Option) {
if opt.Name == "go_package" {
projectPath = path.Dir(protoPath)
packageName = path.Base(opt.Constant.Source)
}
}),
)

return projectPath, packageName
return path.Dir(protoPath), packageName
}

func getServices(definition *proto.Proto) []ProtoService {
Expand All @@ -199,6 +204,7 @@ func getServices(definition *proto.Proto) []ProtoService {
Response: rpc.ReturnsType,
Streaming: rpc.StreamsReturns || rpc.StreamsRequest,
}

service.Methods = append(service.Methods, method)
}
}
Expand Down
60 changes: 59 additions & 1 deletion wrap/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ import "gofr.dev/pkg/gofr"
// Register the gRPC service in your app using the following code in your main.go:
//
// {{ .Package }}.Register{{ $.Service }}ServerWithGofr(app, &grpc.{{ $.Service }}GoFrServer{})
// {{ .Package }}.Register{{ $.Service }}ServerWithGofr(app, &{{ .Package }}.{{ $.Service }}GoFrServer{})
//
// {{ $.Service }}GoFrServer defines the gRPC server implementation.
// Customize the struct with required dependencies and fields as needed.
Expand All @@ -147,5 +147,63 @@ func (s *{{ $.Service }}GoFrServer) {{ .Name }}(ctx *gofr.Context) (any, error)
return &{{ .Response }}{}, nil
}
{{- end }}
`
clientTemplate = `// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
package {{ .Package }}
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
type {{ .Service }}GoFrClient interface {
{{- range .Methods }}
{{ .Name }}(*gofr.Context, *{{ .Request }}) (*{{ .Response }}, error)
{{- end }}
}
type {{ .Service }}ClientWrapper struct {
client {{ .Service }}Client
Container *container.Container
{{ .Service }}GoFrClient
}
func createGRPCConn(host string) (*grpc.ClientConn, error) {
conn, err := grpc.Dial(host, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
return conn, nil
}
func New{{ .Service }}GoFrClient(host string) (*{{ .Service }}ClientWrapper, error) {
conn, err := createGRPCConn(host)
if err != nil {
return &{{ .Service }}ClientWrapper{client: nil}, err
}
res := New{{ .Service }}Client(conn)
return &{{ .Service }}ClientWrapper{
client: res,
}, nil
}
{{- range .Methods }}
func (h *{{ $.Service }}ClientWrapper) {{ .Name }}(ctx *gofr.Context, req *{{ .Request }}) (*{{ .Response }}, error) {
span := ctx.Trace("gRPC-srv-call: {{ .Name }}")
defer span.End()
traceID := span.SpanContext().TraceID().String()
spanID := span.SpanContext().SpanID().String()
md := metadata.Pairs("x-gofr-traceid", traceID, "x-gofr-spanid", spanID)
ctx.Context = metadata.NewOutgoingContext(ctx.Context, md)
return h.client.{{ .Name }}(ctx.Context, req)
}
{{- end }}
`
)

0 comments on commit e092d61

Please sign in to comment.