Skip to content
/ denny Public

common grpc/http server which simplify request handling and logging, support both protocol with same code base

License

Notifications You must be signed in to change notification settings

whatvn/denny

Repository files navigation

denny

common http/grpc server which simplify request handling and logging by combining libraries, framework to be able to

  • support both http and grpc in one controller, write once, support both protocol. See example
  • support class base request controller, one controller for one handler, or you can describe your service in grpc and implement grpc service, denny will then support HTTP/gRPC when you start it in brpc mode, see example
  • make cache usage simpler
  • use open tracing
  • make config reader simpler
  • make logger attached in request context, log should be showed as steps and in only one line for every request

denny is not a http/grpc server from scratch, by now it's based on gin framework and google grpc server, with help of Golang reflection to support both protocol while requires user just minimum about of work. Eq: you just need to write your service in grpc, denny will also support HTTP for your implementation.

denny is different from grpc gateway, grpc gateway uses code generation to generate http proxy call, a request to http enpoint of grpc gateway will also trigger another grpc call to your service. it's http proxy to grpc, with denny, a call to http will only invoke the code you wrote, does not trigger grpc call. It applies also for grpc call. Because of that, using grpc your service has to start with 2 services port, 1 for http and 1 for grpc, denny need only one for both protocol.

It also borrows many component from well known libraries (go-config, beego, logrus...).

usage example

Register discoverable grpc service

using etcd as naming storage

//server
func main() {

	server := denny.NewServer(true)
	// setup grpc server

	grpcServer := denny.NewGrpcServer()
	pb.RegisterHelloServiceServer(grpcServer, new(Hello))
	server.WithGrpcServer(grpcServer)
	
	registry := etcd.New("127.0.0.1:7379", "demo.brpc.svc")
	server.WithRegistry(registry)

	// start server in dual mode
	server.GraceFulStart(":8081")
}
// client

package main

import (
	"context"
	"fmt"
	"github.com/golang/protobuf/ptypes/empty"
	"github.com/whatvn/denny/example/protobuf"
	"github.com/whatvn/denny/naming/etcd"
	"github.com/whatvn/denny/naming"
	"google.golang.org/grpc"
)

func main() {

	registry := etcd.NewResolver("127.0.0.1:7379", "demo.brpc.svc")
	conn, err := grpc.Dial(registry.SvcName(), naming.DefaultBalancePolicy(), grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	client := pb.NewHelloServiceClient(conn)
	response, err := client.SayHelloAnonymous(context.Background(), &empty.Empty{})
	fmt.Println(response, err)
}

using redis as naming storage

//server
func main() {

	server := denny.NewServer(true)
	// setup grpc server

	grpcServer := denny.NewGrpcServer()
	pb.RegisterHelloServiceServer(grpcServer, new(Hello))
	server.WithGrpcServer(grpcServer)
	
	registry := redis.New("127.0.0.1:6379","",  "demo.brpc.svc")
	server.WithRegistry(registry)

	// start server in dual mode
	server.GraceFulStart(":8081")
}
// client

package main

import (
	"context"
	"fmt"
	"github.com/golang/protobuf/ptypes/empty"
	"github.com/whatvn/denny/example/protobuf"
	"github.com/whatvn/denny/naming/redis"
	"github.com/whatvn/denny/naming"
	"google.golang.org/grpc"
)

func main() {

	registry := redis.NewResolver("127.0.0.1:7379", "", "demo.brpc.svc")
	conn, err := grpc.Dial(registry.SvcName(), naming.DefaultBalancePolicy(), grpc.WithInsecure())
	if err != nil {
		panic(err)
	}
	client := pb.NewHelloServiceClient(conn)
	response, err := client.SayHelloAnonymous(context.Background(), &empty.Empty{})
	fmt.Println(response, err)
}

Write grpc code but support both http/grpc

package main

import (
	"context"
	"fmt"
	"github.com/golang/protobuf/ptypes/empty"
	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	"github.com/uber/jaeger-client-go/zipkin"
	"github.com/whatvn/denny"
	pb "github.com/whatvn/denny/example/protobuf"
	"github.com/whatvn/denny/middleware/http"
	"github.com/whatvn/denny/naming/etcd"
	"io"
)

// grpc
type Hello struct{}

// groupPath + "/hello/" + "say-hello"
//
func (s *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	var (
		logger = denny.GetLogger(ctx)
	)
	response := &pb.HelloResponse{
		Reply: "hi",
	}

	logger.WithField("response", response)
	return response, nil
}

// http get request
// when define grpc method with input is empty.Empty object, denny will consider request as get request
// router will be:
// groupPath + "/hello/" + "say-hello-anonymous"
// rule is rootRoute + "/" kebabCase(serviceName) + "/" kebabCase(methodName)

func (s *Hello) SayHelloAnonymous(ctx context.Context, in *empty.Empty) (*pb.HelloResponse, error) {

	var (
		logger = denny.GetLogger(ctx)
	)

	span, ctx := opentracing.StartSpanFromContext(ctx, "sayHello")
	defer span.Finish()
	response := &pb.HelloResponse{
		Reply: "ha",
	}

	logger.WithField("response", response)

	return response, nil
}

type TestController struct {
	denny.Controller
}

func (t *TestController) Handle(ctx *denny.Context) {
	ctx.JSON(200, &pb.HelloResponse{
		Reply: "ha",
	})
}

func newReporterUDP(jaegerAddr string, port int, packetLength int) jaeger.Transport {
	hostString := fmt.Sprintf("%s:%d", jaegerAddr, port)
	transport, err := jaeger.NewUDPTransport(hostString, packetLength)
	if err != nil {
		panic(err)
	}
	return transport
}
func initTracerUDP(jaegerAddr string, port int, packetLength int, serviceName string) (opentracing.Tracer, io.Closer) {
	var (
		propagator = zipkin.NewZipkinB3HTTPHeaderPropagator()
	)

	return jaeger.NewTracer(
		serviceName,
		jaeger.NewConstSampler(true),
		jaeger.NewRemoteReporter(newReporterUDP(jaegerAddr, port, packetLength)),
		jaeger.TracerOptions.Injector(opentracing.HTTPHeaders, propagator),
		jaeger.TracerOptions.Extractor(opentracing.HTTPHeaders, propagator),
		jaeger.TracerOptions.ZipkinSharedRPCSpan(true),
	)
}

func main() {

	// open tracing
	tracer, _ := initTracerUDP(
		"127.0.0.1",
		6831,
		65000,
		"brpc.server.demo",
	)
	opentracing.SetGlobalTracer(tracer)

	server := denny.NewServer(true)
	server.Use(http.Logger())
	group := server.NewGroup("/hi")
	group.Controller("/hi", denny.HttpPost, new(TestController))

	// setup grpc server

	grpcServer := denny.NewGrpcServer()
	pb.RegisterHelloServiceServer(grpcServer, new(Hello))
	server.WithGrpcServer(grpcServer)
	//

	//// then http
	authorized := server.NewGroup("/")
	// http://127.0.0.1:8080/hello/sayhello  (POST)
	// http://127.0.0.1:8080/hello/sayhelloanonymous  (GET)
	authorized.BrpcController(&Hello{})

	// naming registry
	registry := etcd.New("127.0.0.1:7379", "demo.brpc.svc")
	server.WithRegistry(registry)

	// start server in dual mode
	server.GraceFulStart(":8081")
}

For customizing response protobuf JSON Serialization. The Golang JSON serializer doesn’t deal well with Protobuf. Instead, you should use protojson:

package main

import (
	"context"

	"github.com/golang/protobuf/ptypes/empty"
	"github.com/opentracing/opentracing-go"
	"github.com/whatvn/denny"
	pb "github.com/whatvn/denny/example/protobuf"
	"github.com/whatvn/denny/middleware/grpc"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// grpc
type Hello struct{}

// groupPath + "/hello/" + "sayhello"
//
func (s *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	var (
		logger = denny.GetLogger(ctx)
	)
	response := &pb.HelloResponse{
		Reply:     "hi",
		CreatedAt: timestamppb.Now(),
	}

	logger.WithField("response", response)
	return response, nil
}

func (s *Hello) SayHelloAnonymous(ctx context.Context, in *empty.Empty) (*pb.HelloResponseAnonymous, error) {
	var (
		logger = denny.GetLogger(ctx)
	)

	span, ctx := opentracing.StartSpanFromContext(ctx, "sayHello")
	defer span.Finish()
	response := &pb.HelloResponseAnonymous{
		Reply:     "hoho",
		Status:    pb.Status_STATUS_FAIL,
		CreatedAt: timestamppb.Now(),
	}

	logger.WithField("response", response)

	return response, nil
}

func main() {
	server := denny.NewServer(true)
	// Add your custom JSON response serializer
	denny.AddProtoJsonResponseSerializer(
		denny.ProtoJsonResponseSerializer(  // You can use the Proto json serializer with protojson.MarshalOptions or
			protojson.MarshalOptions{       // create your own one with denny.ProtoJsonSerializer function type
				Indent:          "  ",      // Multiline and Indent — print the message on multiple lines, using the provided indent.
				Multiline:       true,
				UseProtoNames:   false,     // Effectively makes the JSON keys snake_cased instead of camelCased.
				EmitUnpopulated: true,      // Explicitly include unpopulated values in the output
				UseEnumNumbers:  false,     // print enums as int instead of their .String() representations. This is what the regular JSON marshaller gave us earlier.
			}))

	// setup grpc server
	grpcServer := denny.NewGrpcServer(grpc.ValidatorInterceptor)
	pb.RegisterHelloServiceServer(grpcServer, new(Hello))
	server.WithGrpcServer(grpcServer)

	//// then http
	authorized := server.NewGroup("/")
	authorized.BrpcController(&Hello{})

	server.GraceFulStart(":8080")
}

When send request to the server

# Without ProtoJsonResponseSerializer
curl http://localhost:8080/hello/say-hello-anonymous

{"reply":"hoho","status":1,"created_at":{"seconds":1621181088,"nanos":984488000}}

# With ProtoJsonResponseSerializer
curl http://localhost:8080/hello/say-hello-anonymous

{"reply":"hoho","status":"STATUS_FAIL","createdAt":"2021-05-16T16:05:59.312303Z"}

setting up simple http request handler

package main

import (
	"github.com/whatvn/denny"
	"github.com/whatvn/denny/middleware/http"
)

type xController struct {
	denny.Controller
}

// define handle function for controller  
func (x xController) Handle(ctx *denny.Context) {
	var (
		logger = denny.GetLogger(ctx)
	)
	logger.AddLog("receive request")
	var str = "hello"
	logger.AddLog("do more thing") // logger middleware will log automatically when request finished
	str += " world"
	ctx.Writer.Write([]byte(str))
}

func main() {
	server := denny.NewServer()
	server.WithMiddleware(http.Logger())
	server.Controller("/", denny.HttpGet, &xController{})
	server.GraceFulStart()
}

Reading config

package main

import (
	"fmt"
	"github.com/whatvn/denny/config"
	"os"
	"path/filepath"
	"time"
)

func configFile() (*os.File, error) {
	data := []byte(`{"foo": "bar", "denny": {"sister": "jenny"}}`)
	path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano()))
	fh, err := os.Create(path)
	if err != nil {
		return nil, err
	}
	_, err = fh.Write(data)
	if err != nil {
		return nil, err
	}

	return fh, nil
}

func main() {
	f, err := configFile()
	if err != nil {
		fmt.Println(err)
	}
	// read config from file
	config.New(f.Name())
	fmt.Println(config.GetString("foo"))
	fmt.Println(config.GetString("denny", "sister"))

	// config from evn takes higher priority
	os.Setenv("foo", "barbar")
	os.Setenv("denny_sister", "Jenny")
	config.Reload()
	fmt.Println(config.GetString("foo"))
	fmt.Println(config.GetString("denny", "sister"))
}

limit

Denny uses etcd as naming registry, but etcd packaging is somewhat complicated, new version links to old version, old version links to older version which is very difficult to optimize import, so currently it use a fork version of etcd here. Look at this issue to track

About

common grpc/http server which simplify request handling and logging, support both protocol with same code base

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages