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...).
//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)
}
//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)
}
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"}
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()
}
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"))
}
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