├── middleware ├── validator.go ├── middleware.go ├── grpc │ ├── pre_validate.go │ └── logger.go └── http │ ├── logger.go │ └── ot │ ├── options.go │ └── middleware.go ├── cache ├── redis_cluster.go ├── cache_test.go ├── cache.go ├── redis.go └── memory.go ├── go_config ├── encoder │ ├── encoder.go │ ├── xml │ │ └── xml.go │ ├── json │ │ └── json.go │ ├── yaml │ │ └── yaml.go │ ├── hcl │ │ └── hcl.go │ └── toml │ │ └── toml.go ├── source │ ├── changeset.go │ ├── file │ │ ├── format.go │ │ ├── options.go │ │ ├── format_test.go │ │ ├── file_test.go │ │ ├── watcher.go │ │ ├── file.go │ │ ├── watcher_linux.go │ │ └── README.md │ ├── memory │ │ ├── watcher.go │ │ ├── README.md │ │ ├── options.go │ │ └── memory.go │ ├── env │ │ ├── watcher.go │ │ ├── options.go │ │ ├── README.md │ │ ├── env_test.go │ │ └── env.go │ ├── flag │ │ ├── options.go │ │ ├── README.md │ │ ├── flag_test.go │ │ └── flag.go │ ├── noop.go │ ├── options.go │ ├── source.go │ └── etcd │ │ ├── README.md │ │ ├── watcher.go │ │ ├── options.go │ │ └── etcd.go ├── reader │ ├── preprocessor.go │ ├── json │ │ ├── json_test.go │ │ ├── values_test.go │ │ ├── json.go │ │ └── values.go │ ├── reader.go │ ├── options.go │ └── preprocessor_test.go ├── loader │ ├── memory │ │ ├── options.go │ │ └── memory.go │ └── loader.go ├── options │ ├── default.go │ └── options.go ├── options.go ├── value.go ├── README.md ├── config.go ├── default_test.go └── default.go ├── method.go ├── binding.go ├── .gitignore ├── controller.go ├── config ├── options.go ├── config.go └── config_test.go ├── logger.go ├── example ├── cli.go ├── protobuf │ ├── hello.proto │ ├── hello.pb.validate.go │ └── hello.pb.go ├── config.go ├── handler1.go ├── handler.go └── brpc.go ├── naming ├── redis │ ├── redis.go │ ├── naming.go │ └── resolver.go ├── registry.go └── etcd │ ├── etcd.go │ ├── naming.go │ └── resolver.go ├── LICENSE.txt ├── log └── log.go ├── grpc.go ├── go.mod ├── denny_test.go ├── README.md └── denny.go /middleware/validator.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | type IValidator interface { 4 | Validate() error 5 | } 6 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | // middleware contains built-in/necessary middleware (http) and interceptor (grpc) 2 | package middleware 3 | -------------------------------------------------------------------------------- /cache/redis_cluster.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | // 4 | //func NewRedisCluster() { 5 | // slotFunc := func() { 6 | // slot := []redisCli.ClusterNode{ 7 | // 8 | // } 9 | // } 10 | // 11 | //} 12 | -------------------------------------------------------------------------------- /go_config/encoder/encoder.go: -------------------------------------------------------------------------------- 1 | // Package encoder handles source encoding formats 2 | package encoder 3 | 4 | type Encoder interface { 5 | Encode(interface{}) ([]byte, error) 6 | Decode([]byte, interface{}) error 7 | String() string 8 | } 9 | -------------------------------------------------------------------------------- /method.go: -------------------------------------------------------------------------------- 1 | package denny 2 | 3 | type HttpMethod string 4 | 5 | const ( 6 | HttpGet HttpMethod = "GET" 7 | HttpPost HttpMethod = "POST" 8 | HttpPatch HttpMethod = "PATCH" 9 | HttpOption HttpMethod = "OPTION" 10 | HttpDelete HttpMethod = "DELETE" 11 | ) 12 | -------------------------------------------------------------------------------- /go_config/source/changeset.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | ) 7 | 8 | // Sum returns the md5 checksum of the ChangeSet data 9 | func (c *ChangeSet) Sum() string { 10 | h := md5.New() 11 | h.Write(c.Data) 12 | return fmt.Sprintf("%x", h.Sum(nil)) 13 | } 14 | -------------------------------------------------------------------------------- /go_config/source/file/format.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/whatvn/denny/go_config/encoder" 7 | ) 8 | 9 | func format(p string, e encoder.Encoder) string { 10 | parts := strings.Split(p, ".") 11 | if len(parts) > 1 { 12 | return parts[len(parts)-1] 13 | } 14 | return e.String() 15 | } 16 | -------------------------------------------------------------------------------- /go_config/source/file/options.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/whatvn/denny/go_config/source" 7 | ) 8 | 9 | type filePathKey struct{} 10 | 11 | // WithPath sets the path to file 12 | func WithPath(p string) source.Option { 13 | return func(o *source.Options) { 14 | if o.Context == nil { 15 | o.Context = context.Background() 16 | } 17 | o.Context = context.WithValue(o.Context, filePathKey{}, p) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /binding.go: -------------------------------------------------------------------------------- 1 | package denny 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/gin-gonic/gin/binding" 6 | ) 7 | 8 | var Validator = binding.Validator 9 | 10 | func Binding(ctx *gin.Context) binding.Binding { 11 | if ctx == nil { 12 | return nil 13 | } 14 | switch ctx.ContentType() { 15 | case binding.MIMEPOSTForm: 16 | return binding.FormPost 17 | case binding.MIMEXML: 18 | return binding.XML 19 | default: 20 | return binding.JSON 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go ### 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | ### Go Patch ### 19 | /vendor/ 20 | /Godeps/ 21 | 22 | .DS_Store 23 | .vscode/ 24 | .idea/ 25 | 26 | *.env 27 | bin/ -------------------------------------------------------------------------------- /go_config/source/memory/watcher.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "github.com/whatvn/denny/go_config/source" 5 | ) 6 | 7 | type watcher struct { 8 | Id string 9 | Updates chan *source.ChangeSet 10 | Source *memory 11 | } 12 | 13 | func (w *watcher) Next() (*source.ChangeSet, error) { 14 | cs := <-w.Updates 15 | return cs, nil 16 | } 17 | 18 | func (w *watcher) Stop() error { 19 | w.Source.Lock() 20 | delete(w.Source.Watchers, w.Id) 21 | w.Source.Unlock() 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /go_config/source/env/watcher.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "github.com/whatvn/denny/go_config/source" 5 | ) 6 | 7 | type watcher struct { 8 | exit chan struct{} 9 | } 10 | 11 | func (w *watcher) Next() (*source.ChangeSet, error) { 12 | <-w.exit 13 | 14 | return nil, source.ErrWatcherStopped 15 | } 16 | 17 | func (w *watcher) Stop() error { 18 | close(w.exit) 19 | return nil 20 | } 21 | 22 | func newWatcher() (source.Watcher, error) { 23 | return &watcher{exit: make(chan struct{})}, nil 24 | } 25 | -------------------------------------------------------------------------------- /middleware/grpc/pre_validate.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/whatvn/denny/middleware" 7 | "google.golang.org/grpc" 8 | ) 9 | 10 | func ValidatorInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 11 | if v, ok := req.(middleware.IValidator); ok { 12 | if err := v.Validate(); err != nil { 13 | return nil, err 14 | } 15 | } 16 | 17 | resp, err = handler(ctx, req) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /go_config/encoder/xml/xml.go: -------------------------------------------------------------------------------- 1 | package xml 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/whatvn/denny/go_config/encoder" 7 | ) 8 | 9 | type xmlEncoder struct{} 10 | 11 | func (x xmlEncoder) Encode(v interface{}) ([]byte, error) { 12 | return xml.Marshal(v) 13 | } 14 | 15 | func (x xmlEncoder) Decode(d []byte, v interface{}) error { 16 | return xml.Unmarshal(d, v) 17 | } 18 | 19 | func (x xmlEncoder) String() string { 20 | return "xml" 21 | } 22 | 23 | func NewEncoder() encoder.Encoder { 24 | return xmlEncoder{} 25 | } 26 | -------------------------------------------------------------------------------- /go_config/reader/preprocessor.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | ) 7 | 8 | func ReplaceEnvVars(raw []byte) ([]byte, error) { 9 | re := regexp.MustCompile(`\$\{([A-Za-z0-9_]+)\}`) 10 | if re.Match(raw) { 11 | dataS := string(raw) 12 | res := re.ReplaceAllStringFunc(dataS, replaceEnvVars) 13 | return []byte(res), nil 14 | } else { 15 | return raw, nil 16 | } 17 | } 18 | 19 | func replaceEnvVars(element string) string { 20 | v := element[2 : len(element)-1] 21 | el := os.Getenv(v) 22 | return el 23 | } 24 | -------------------------------------------------------------------------------- /go_config/encoder/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/whatvn/denny/go_config/encoder" 7 | ) 8 | 9 | type jsonEncoder struct{} 10 | 11 | func (j jsonEncoder) Encode(v interface{}) ([]byte, error) { 12 | return json.Marshal(v) 13 | } 14 | 15 | func (j jsonEncoder) Decode(d []byte, v interface{}) error { 16 | return json.Unmarshal(d, v) 17 | } 18 | 19 | func (j jsonEncoder) String() string { 20 | return "json" 21 | } 22 | 23 | func NewEncoder() encoder.Encoder { 24 | return jsonEncoder{} 25 | } 26 | -------------------------------------------------------------------------------- /go_config/encoder/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "github.com/ghodss/yaml" 5 | "github.com/whatvn/denny/go_config/encoder" 6 | ) 7 | 8 | type yamlEncoder struct{} 9 | 10 | func (y yamlEncoder) Encode(v interface{}) ([]byte, error) { 11 | return yaml.Marshal(v) 12 | } 13 | 14 | func (y yamlEncoder) Decode(d []byte, v interface{}) error { 15 | return yaml.Unmarshal(d, v) 16 | } 17 | 18 | func (y yamlEncoder) String() string { 19 | return "yaml" 20 | } 21 | 22 | func NewEncoder() encoder.Encoder { 23 | return yamlEncoder{} 24 | } 25 | -------------------------------------------------------------------------------- /controller.go: -------------------------------------------------------------------------------- 1 | package denny 2 | 3 | import ( 4 | "github.com/gin-gonic/gin/binding" 5 | "github.com/whatvn/denny/log" 6 | ) 7 | 8 | type controller interface { 9 | Handle(*Context) 10 | init() 11 | SetValidator(validator binding.StructValidator) 12 | } 13 | 14 | type Controller struct { 15 | binding.StructValidator 16 | *log.Log 17 | } 18 | 19 | func (c *Controller) init() { 20 | c.Log = log.New() 21 | c.StructValidator = binding.Validator 22 | } 23 | 24 | func (c *Controller) SetValidator(v binding.StructValidator) { 25 | c.StructValidator = v 26 | } 27 | -------------------------------------------------------------------------------- /go_config/encoder/hcl/hcl.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/hashicorp/hcl" 7 | "github.com/whatvn/denny/go_config/encoder" 8 | ) 9 | 10 | type hclEncoder struct{} 11 | 12 | func (h hclEncoder) Encode(v interface{}) ([]byte, error) { 13 | return json.Marshal(v) 14 | } 15 | 16 | func (h hclEncoder) Decode(d []byte, v interface{}) error { 17 | return hcl.Unmarshal(d, v) 18 | } 19 | 20 | func (h hclEncoder) String() string { 21 | return "hcl" 22 | } 23 | 24 | func NewEncoder() encoder.Encoder { 25 | return hclEncoder{} 26 | } 27 | -------------------------------------------------------------------------------- /go_config/source/flag/options.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/whatvn/denny/go_config/source" 7 | ) 8 | 9 | type includeUnsetKey struct{} 10 | 11 | // IncludeUnset toggles the loading of unset flags and their respective default values. 12 | // Default behavior is to ignore any unset flags. 13 | func IncludeUnset(b bool) source.Option { 14 | return func(o *source.Options) { 15 | if o.Context == nil { 16 | o.Context = context.Background() 17 | } 18 | o.Context = context.WithValue(o.Context, includeUnsetKey{}, true) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /go_config/source/noop.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type noopWatcher struct { 8 | exit chan struct{} 9 | } 10 | 11 | func (w *noopWatcher) Next() (*ChangeSet, error) { 12 | <-w.exit 13 | 14 | return nil, errors.New("noopWatcher stopped") 15 | } 16 | 17 | func (w *noopWatcher) Stop() error { 18 | close(w.exit) 19 | return nil 20 | } 21 | 22 | // NewNoopWatcher returns a watcher that blocks on Next() until Stop() is called. 23 | func NewNoopWatcher() (Watcher, error) { 24 | return &noopWatcher{exit: make(chan struct{})}, nil 25 | } 26 | -------------------------------------------------------------------------------- /go_config/loader/memory/options.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "github.com/whatvn/denny/go_config/loader" 5 | "github.com/whatvn/denny/go_config/reader" 6 | "github.com/whatvn/denny/go_config/source" 7 | ) 8 | 9 | // WithSource appends a source to list of sources 10 | func WithSource(s source.Source) loader.Option { 11 | return func(o *loader.Options) { 12 | o.Source = append(o.Source, s) 13 | } 14 | } 15 | 16 | // WithReader sets the config reader 17 | func WithReader(r reader.Reader) loader.Option { 18 | return func(o *loader.Options) { 19 | o.Reader = r 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/whatvn/denny/go_config/source" 5 | "github.com/whatvn/denny/go_config/source/etcd" 6 | ) 7 | 8 | func WithEtcdAddress(addr ...string) source.Option { 9 | return etcd.WithAddress(addr...) 10 | } 11 | 12 | func WithEtcdAuth(user, pass string) source.Option { 13 | return etcd.BasicAuth(user, pass) 14 | } 15 | 16 | func WithEtcdTLSAuth(certFile, keyFile, caFile string) source.Option { 17 | return etcd.TLSAuth(caFile, certFile, keyFile) 18 | } 19 | 20 | func WithPath(path string) source.Option { 21 | return etcd.WithPath(path) 22 | } 23 | -------------------------------------------------------------------------------- /go_config/source/file/format_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/whatvn/denny/go_config/source" 7 | ) 8 | 9 | func TestFormat(t *testing.T) { 10 | opts := source.NewOptions() 11 | e := opts.Encoder 12 | 13 | testCases := []struct { 14 | p string 15 | f string 16 | }{ 17 | {"/foo/bar.json", "json"}, 18 | {"/foo/bar.yaml", "yaml"}, 19 | {"/foo/bar.xml", "xml"}, 20 | {"/foo/bar.conf.ini", "ini"}, 21 | {"conf", e.String()}, 22 | } 23 | 24 | for _, d := range testCases { 25 | f := format(d.p, e) 26 | if f != d.f { 27 | t.Fatalf("%s: expected %s got %s", d.p, d.f, f) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package denny 2 | 3 | import ( 4 | "context" 5 | "github.com/whatvn/denny/log" 6 | ) 7 | 8 | func GetLogger(ctx context.Context) *log.Log { 9 | var ( 10 | logger interface{} 11 | ) 12 | if ctx, ok := ctx.(*Context); ok { 13 | logger, ok := ctx.Get(log.LogKey) 14 | if !ok { 15 | logger := log.New() 16 | ctx.Set(log.LogKey, logger) 17 | return logger 18 | } 19 | return logger.(*log.Log) 20 | } 21 | logger, ok := ctx.Value(log.LogKey).(*log.Log) 22 | if !ok { 23 | logger := log.New() 24 | ctx = context.WithValue( 25 | ctx, 26 | log.LogKey, logger) 27 | return logger 28 | } 29 | return logger.(*log.Log) 30 | } 31 | -------------------------------------------------------------------------------- /cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestMemoryCache(t *testing.T) { 9 | 10 | var ( 11 | cache = NewMemoryCache(Config{ 12 | GcDuration: 2, 13 | }) 14 | key = "name" 15 | value = "denny" 16 | ) 17 | 18 | cache.Set(key, value, 2) 19 | v := cache.Get(key) 20 | if v.(string) != value { 21 | t.Error("wrong value return") 22 | } 23 | 24 | time.Sleep(time.Duration(3) * time.Second) 25 | v = cache.Get(key) 26 | if v != nil { 27 | t.Error("gc cannot run") 28 | } 29 | cache.Set(key, value, 0) 30 | cache.Delete(key) 31 | v = cache.Get(key) 32 | if v != nil { 33 | t.Error("cannot delete key") 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /go_config/options/default.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | type defaultOptions struct { 4 | opts *Values 5 | } 6 | 7 | type stringKey struct{} 8 | 9 | func (d *defaultOptions) Init(opts ...Option) error { 10 | if d.opts == nil { 11 | d.opts = new(Values) 12 | } 13 | for _, o := range opts { 14 | if err := d.opts.Option(o); err != nil { 15 | return err 16 | } 17 | } 18 | return nil 19 | } 20 | 21 | func (d *defaultOptions) Values() *Values { 22 | return d.opts 23 | } 24 | 25 | func (d *defaultOptions) String() string { 26 | if d.opts == nil { 27 | d.opts = new(Values) 28 | } 29 | n, ok := d.opts.Get(stringKey{}) 30 | if ok { 31 | return n.(string) 32 | } 33 | return "Values" 34 | } 35 | -------------------------------------------------------------------------------- /go_config/encoder/toml/toml.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/BurntSushi/toml" 7 | "github.com/whatvn/denny/go_config/encoder" 8 | ) 9 | 10 | type tomlEncoder struct{} 11 | 12 | func (t tomlEncoder) Encode(v interface{}) ([]byte, error) { 13 | b := bytes.NewBuffer(nil) 14 | defer b.Reset() 15 | err := toml.NewEncoder(b).Encode(v) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return b.Bytes(), nil 20 | } 21 | 22 | func (t tomlEncoder) Decode(d []byte, v interface{}) error { 23 | return toml.Unmarshal(d, v) 24 | } 25 | 26 | func (t tomlEncoder) String() string { 27 | return "toml" 28 | } 29 | 30 | func NewEncoder() encoder.Encoder { 31 | return tomlEncoder{} 32 | } 33 | -------------------------------------------------------------------------------- /go_config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/whatvn/denny/go_config/loader" 5 | "github.com/whatvn/denny/go_config/reader" 6 | "github.com/whatvn/denny/go_config/source" 7 | ) 8 | 9 | // WithLoader sets the loader for manager config 10 | func WithLoader(l loader.Loader) Option { 11 | return func(o *Options) { 12 | o.Loader = l 13 | } 14 | } 15 | 16 | // WithSource appends a source to list of sources 17 | func WithSource(s source.Source) Option { 18 | return func(o *Options) { 19 | o.Source = append(o.Source, s) 20 | } 21 | } 22 | 23 | // WithReader sets the config reader 24 | func WithReader(r reader.Reader) Option { 25 | return func(o *Options) { 26 | o.Reader = r 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/whatvn/denny/naming/redis" 6 | 7 | pb "github.com/whatvn/denny/example/protobuf" 8 | "github.com/whatvn/denny/naming" 9 | "golang.org/x/net/context" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | func main() { 14 | 15 | registry := redis.NewResolver("127.0.0.1:6379", "", "demo.brpc.svc") 16 | conn, err := grpc.Dial(registry.SvcName(), naming.DefaultBalancePolicy(), grpc.WithInsecure()) 17 | if err != nil { 18 | panic(err) 19 | } 20 | client := pb.NewHelloServiceClient(conn) 21 | 22 | response, err := client.SayHello(context.Background(), &pb.HelloRequest{Greeting: ""}) 23 | if err != nil { 24 | fmt.Println(err) 25 | } 26 | 27 | fmt.Println(response, err) 28 | 29 | } 30 | -------------------------------------------------------------------------------- /go_config/source/file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestFile(t *testing.T) { 12 | data := []byte(`{"foo": "bar"}`) 13 | path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano())) 14 | fh, err := os.Create(path) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | defer func() { 19 | fh.Close() 20 | os.Remove(path) 21 | }() 22 | 23 | _, err = fh.Write(data) 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | 28 | f := NewSource(WithPath(path)) 29 | c, err := f.Read() 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | t.Logf("%+v", c) 34 | if string(c.Data) != string(data) { 35 | t.Error("data from file does not match") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /go_config/source/options.go: -------------------------------------------------------------------------------- 1 | package source 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/whatvn/denny/go_config/encoder" 7 | "github.com/whatvn/denny/go_config/encoder/json" 8 | ) 9 | 10 | type Options struct { 11 | // Encoder 12 | Encoder encoder.Encoder 13 | 14 | // for alternative data 15 | Context context.Context 16 | } 17 | 18 | type Option func(o *Options) 19 | 20 | func NewOptions(opts ...Option) Options { 21 | options := Options{ 22 | Encoder: json.NewEncoder(), 23 | Context: context.Background(), 24 | } 25 | 26 | for _, o := range opts { 27 | o(&options) 28 | } 29 | 30 | return options 31 | } 32 | 33 | // WithEncoder sets the source encoder 34 | func WithEncoder(e encoder.Encoder) Option { 35 | return func(o *Options) { 36 | o.Encoder = e 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /go_config/source/source.go: -------------------------------------------------------------------------------- 1 | // Package source is the interface for sources 2 | package source 3 | 4 | import ( 5 | "errors" 6 | "time" 7 | ) 8 | 9 | var ( 10 | // ErrWatcherStopped is returned when source watcher has been stopped 11 | ErrWatcherStopped = errors.New("watcher stopped") 12 | ) 13 | 14 | // Source is the source from which config is loaded 15 | type Source interface { 16 | Read() (*ChangeSet, error) 17 | Watch() (Watcher, error) 18 | String() string 19 | } 20 | 21 | // ChangeSet represents a set of changes from a source 22 | type ChangeSet struct { 23 | Data []byte 24 | Checksum string 25 | Format string 26 | Source string 27 | Timestamp time.Time 28 | } 29 | 30 | // Watcher watches a source for changes 31 | type Watcher interface { 32 | Next() (*ChangeSet, error) 33 | Stop() error 34 | } 35 | -------------------------------------------------------------------------------- /go_config/source/memory/README.md: -------------------------------------------------------------------------------- 1 | # Memory Source 2 | 3 | The memory source provides in-memory data as a source 4 | 5 | ## Memory Format 6 | 7 | The expected data format is json 8 | 9 | ```json 10 | data := []byte(`{ 11 | "hosts": { 12 | "database": { 13 | "address": "10.0.0.1", 14 | "port": 3306 15 | }, 16 | "cache": { 17 | "address": "10.0.0.2", 18 | "port": 6379 19 | } 20 | } 21 | }`) 22 | ``` 23 | 24 | ## New Source 25 | 26 | Specify source with data 27 | 28 | ```go 29 | memorySource := memory.NewSource( 30 | memory.WithJSON(data), 31 | ) 32 | ``` 33 | 34 | ## Load Source 35 | 36 | Load the source into config 37 | 38 | ```go 39 | // Create new config 40 | conf := config.NewConfig() 41 | 42 | // Load memory source 43 | conf.Load(memorySource) 44 | ``` 45 | -------------------------------------------------------------------------------- /go_config/reader/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/whatvn/denny/go_config/source" 7 | ) 8 | 9 | func TestReader(t *testing.T) { 10 | data := []byte(`{"foo": "bar", "baz": {"bar": "cat"}}`) 11 | 12 | testData := []struct { 13 | path []string 14 | value string 15 | }{ 16 | { 17 | []string{"foo"}, 18 | "bar", 19 | }, 20 | { 21 | []string{"baz", "bar"}, 22 | "cat", 23 | }, 24 | } 25 | 26 | r := NewReader() 27 | 28 | c, err := r.Merge(&source.ChangeSet{Data: data}, &source.ChangeSet{}) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | values, err := r.Values(c) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | for _, test := range testData { 39 | if v := values.Get(test.path...).String(""); v != test.value { 40 | t.Fatalf("Expected %s got %s for path %v", test.value, v, test.path) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /naming/redis/redis.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | redisCli "github.com/go-redis/redis" 5 | "github.com/whatvn/denny/log" 6 | "github.com/whatvn/denny/naming" 7 | "google.golang.org/grpc/resolver" 8 | ) 9 | 10 | type redis struct { 11 | cli *redisCli.Client 12 | *log.Log 13 | shutdown chan interface{} 14 | cc resolver.ClientConn 15 | serviceName string 16 | } 17 | 18 | func New(redisAddr, redisPassword, serviceName string) naming.Registry { 19 | client := redisCli.NewClient(&redisCli.Options{ 20 | Addr: redisAddr, 21 | Password: redisPassword, 22 | }) 23 | 24 | registry := &redis{ 25 | cli: client, 26 | Log: log.New(), 27 | serviceName: serviceName, 28 | shutdown: make(chan interface{}, 1), 29 | } 30 | registry.WithField("redis", redisAddr) 31 | if len(redisPassword) > 0 { 32 | registry.WithField("redisPassword", redisPassword) 33 | } 34 | return registry 35 | } 36 | -------------------------------------------------------------------------------- /example/protobuf/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | import "google/protobuf/empty.proto"; 3 | import "validate/validate.proto"; 4 | import "google/protobuf/timestamp.proto"; 5 | 6 | option go_package = "example/protobuf"; 7 | package pb; 8 | 9 | //option go_package = "example.com/foo/bar"; 10 | // protoc -I=. *.proto --go_out=plugins=grpc:. 11 | 12 | enum Status { 13 | STATUS_SUCCESS= 0; 14 | STATUS_FAIL = 1; 15 | } 16 | 17 | message HelloRequest { 18 | string greeting = 1 [(validate.rules).string.min_len = 1]; 19 | } 20 | 21 | message HelloResponse { 22 | string reply = 1; 23 | google.protobuf.Timestamp created_at = 2; 24 | } 25 | 26 | message HelloResponseAnonymous { 27 | string reply = 1; 28 | Status status = 2; 29 | google.protobuf.Timestamp created_at = 3; 30 | } 31 | 32 | service HelloService { 33 | rpc SayHello(HelloRequest) returns (HelloResponse); 34 | rpc SayHelloAnonymous(google.protobuf.Empty) returns (HelloResponseAnonymous); 35 | } -------------------------------------------------------------------------------- /go_config/value.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/whatvn/denny/go_config/reader" 7 | ) 8 | 9 | type value struct{} 10 | 11 | func newValue() reader.Value { 12 | return new(value) 13 | } 14 | 15 | func (v *value) Bool(def bool) bool { 16 | return false 17 | } 18 | 19 | func (v *value) Int(def int) int { 20 | return 0 21 | } 22 | 23 | func (v *value) String(def string) string { 24 | return "" 25 | } 26 | 27 | func (v *value) Float64(def float64) float64 { 28 | return 0.0 29 | } 30 | 31 | func (v *value) Duration(def time.Duration) time.Duration { 32 | return time.Duration(0) 33 | } 34 | 35 | func (v *value) StringSlice(def []string) []string { 36 | return nil 37 | } 38 | 39 | func (v *value) StringMap(def map[string]string) map[string]string { 40 | return map[string]string{} 41 | } 42 | 43 | func (v *value) Scan(val interface{}) error { 44 | return nil 45 | } 46 | 47 | func (v *value) Bytes() []byte { 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /go_config/source/flag/README.md: -------------------------------------------------------------------------------- 1 | # Flag Source 2 | 3 | The flag source reads config from flags 4 | 5 | ## Format 6 | 7 | We expect the use of the `flag` package. Upper case flags will be lower cased. Dashes will be used as delimiters. 8 | 9 | ### Example 10 | 11 | ``` 12 | dbAddress := flag.String("database_address", "127.0.0.1", "the db address") 13 | dbPort := flag.Int("database_port", 3306, "the db port) 14 | ``` 15 | 16 | Becomes 17 | 18 | ```json 19 | { 20 | "database": { 21 | "address": "127.0.0.1", 22 | "port": 3306 23 | } 24 | } 25 | ``` 26 | 27 | ## New Source 28 | 29 | ```go 30 | flagSource := flag.NewSource( 31 | // optionally enable reading of unset flags and their default 32 | // values into config, defaults to false 33 | IncludeUnset(true) 34 | ) 35 | ``` 36 | 37 | ## Load Source 38 | 39 | Load the source into config 40 | 41 | ```go 42 | // Create new config 43 | conf := config.NewConfig() 44 | 45 | // Load flag source 46 | conf.Load(flagSource) 47 | ``` 48 | -------------------------------------------------------------------------------- /go_config/reader/reader.go: -------------------------------------------------------------------------------- 1 | // Package reader parses change sets and provides config values 2 | package reader 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/whatvn/denny/go_config/source" 8 | ) 9 | 10 | // Reader is an interface for merging changesets 11 | type Reader interface { 12 | Merge(...*source.ChangeSet) (*source.ChangeSet, error) 13 | Values(*source.ChangeSet) (Values, error) 14 | String() string 15 | } 16 | 17 | // Values is returned by the reader 18 | type Values interface { 19 | Bytes() []byte 20 | Get(path ...string) Value 21 | Map() map[string]interface{} 22 | Scan(v interface{}) error 23 | } 24 | 25 | // Value represents a value of any type 26 | type Value interface { 27 | Bool(def bool) bool 28 | Int(def int) int 29 | String(def string) string 30 | Float64(def float64) float64 31 | Duration(def time.Duration) time.Duration 32 | StringSlice(def []string) []string 33 | StringMap(def map[string]string) map[string]string 34 | Scan(val interface{}) error 35 | Bytes() []byte 36 | } 37 | -------------------------------------------------------------------------------- /go_config/source/memory/options.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/whatvn/denny/go_config/source" 7 | ) 8 | 9 | type changeSetKey struct{} 10 | 11 | func withData(d []byte, f string) source.Option { 12 | return func(o *source.Options) { 13 | if o.Context == nil { 14 | o.Context = context.Background() 15 | } 16 | o.Context = context.WithValue(o.Context, changeSetKey{}, &source.ChangeSet{ 17 | Data: d, 18 | Format: f, 19 | }) 20 | } 21 | } 22 | 23 | // WithChangeSet allows a changeset to be set 24 | func WithChangeSet(cs *source.ChangeSet) source.Option { 25 | return func(o *source.Options) { 26 | if o.Context == nil { 27 | o.Context = context.Background() 28 | } 29 | o.Context = context.WithValue(o.Context, changeSetKey{}, cs) 30 | } 31 | } 32 | 33 | // WithJSON allows the source data to be set to json 34 | func WithJSON(d []byte) source.Option { 35 | return withData(d, "json") 36 | } 37 | 38 | // WithYAML allows the source data to be set to yaml 39 | func WithYAML(d []byte) source.Option { 40 | return withData(d, "yaml") 41 | } 42 | -------------------------------------------------------------------------------- /go_config/reader/options.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "github.com/whatvn/denny/go_config/encoder" 5 | "github.com/whatvn/denny/go_config/encoder/hcl" 6 | "github.com/whatvn/denny/go_config/encoder/json" 7 | "github.com/whatvn/denny/go_config/encoder/toml" 8 | "github.com/whatvn/denny/go_config/encoder/xml" 9 | "github.com/whatvn/denny/go_config/encoder/yaml" 10 | ) 11 | 12 | type Options struct { 13 | Encoding map[string]encoder.Encoder 14 | } 15 | 16 | type Option func(o *Options) 17 | 18 | func NewOptions(opts ...Option) Options { 19 | options := Options{ 20 | Encoding: map[string]encoder.Encoder{ 21 | "json": json.NewEncoder(), 22 | "yaml": yaml.NewEncoder(), 23 | "toml": toml.NewEncoder(), 24 | "xml": xml.NewEncoder(), 25 | "hcl": hcl.NewEncoder(), 26 | "yml": yaml.NewEncoder(), 27 | }, 28 | } 29 | for _, o := range opts { 30 | o(&options) 31 | } 32 | return options 33 | } 34 | 35 | func WithEncoder(e encoder.Encoder) Option { 36 | return func(o *Options) { 37 | if o.Encoding == nil { 38 | o.Encoding = make(map[string]encoder.Encoder) 39 | } 40 | o.Encoding[e.String()] = e 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | var ( 9 | ValueNotExistError = errors.New("value not exist") 10 | InvalidValueTypeError = errors.New("invalid value type") 11 | ) 12 | 13 | type Cache interface { 14 | // get cached value by key. 15 | Get(key string) interface{} 16 | // GetOrElse return value if it exists, else warmup using warmup function 17 | GetOrElse(key string, warmUpFunc func(key string) interface{}, expire ...int64) interface{} 18 | // GetMulti is a batch version of Get. 19 | GetMulti(keys []string) []interface{} 20 | // set cached value with key and expire time. 21 | Set(key string, val interface{}, expire int64) 22 | // delete cached value by key. 23 | Delete(key string) 24 | // increase cached int value by key, as a counter. 25 | Incr(key string) error 26 | // decrease cached int value by key, as a counter. 27 | Decr(key string) error 28 | // check if cached value exists or not. 29 | IsExist(key string) bool 30 | // clear all cache. 31 | ClearAll() 32 | // start gc routine based on config string settings. 33 | runGc(config Config) 34 | } 35 | 36 | type Config struct { 37 | GcDuration time.Duration 38 | GcEvery int //second 39 | } 40 | -------------------------------------------------------------------------------- /go_config/source/etcd/README.md: -------------------------------------------------------------------------------- 1 | # Etcd Source 2 | 3 | The etcd source reads config from etcd key/values 4 | 5 | This source supports etcd version 3 and beyond. 6 | 7 | ## Etcd Format 8 | 9 | The etcd source expects keys under the default prefix `/micro/config` (prefix can be changed) 10 | 11 | Values are expected to be JSON 12 | 13 | ``` 14 | // set database 15 | etcdctl put /micro/config/database '{"address": "10.0.0.1", "port": 3306}' 16 | // set cache 17 | etcdctl put /micro/config/cache '{"address": "10.0.0.2", "port": 6379}' 18 | ``` 19 | 20 | Keys are split on `/` so access becomes 21 | 22 | ``` 23 | conf.Get("micro", "config", "database") 24 | ``` 25 | 26 | ## New Source 27 | 28 | Specify source with data 29 | 30 | ```go 31 | etcdSource := etcd.NewSource( 32 | // optionally specify etcd address; default to localhost:8500 33 | etcd.WithAddress("10.0.0.10:8500"), 34 | // optionally specify prefix; defaults to /micro/config 35 | etcd.WithPrefix("/my/prefix"), 36 | // optionally strip the provided prefix from the keys, defaults to false 37 | etcd.StripPrefix(true), 38 | ) 39 | ``` 40 | 41 | ## Load Source 42 | 43 | Load the source into config 44 | 45 | ```go 46 | // Create new config 47 | conf := config.NewConfig() 48 | 49 | // Load file source 50 | conf.Load(etcdSource) 51 | ``` 52 | -------------------------------------------------------------------------------- /go_config/source/env/options.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "context" 5 | 6 | "strings" 7 | 8 | "github.com/whatvn/denny/go_config/source" 9 | ) 10 | 11 | type strippedPrefixKey struct{} 12 | type prefixKey struct{} 13 | 14 | // WithStrippedPrefix sets the environment variable prefixes to scope to. 15 | // These prefixes will be removed from the actual config entries. 16 | func WithStrippedPrefix(p ...string) source.Option { 17 | return func(o *source.Options) { 18 | if o.Context == nil { 19 | o.Context = context.Background() 20 | } 21 | 22 | o.Context = context.WithValue(o.Context, strippedPrefixKey{}, appendUnderscore(p)) 23 | } 24 | } 25 | 26 | // WithPrefix sets the environment variable prefixes to scope to. 27 | // These prefixes will not be removed. Each prefix will be considered a top level config entry. 28 | func WithPrefix(p ...string) source.Option { 29 | return func(o *source.Options) { 30 | if o.Context == nil { 31 | o.Context = context.Background() 32 | } 33 | o.Context = context.WithValue(o.Context, prefixKey{}, appendUnderscore(p)) 34 | } 35 | } 36 | 37 | func appendUnderscore(prefixes []string) []string { 38 | var result []string 39 | for _, p := range prefixes { 40 | if !strings.HasSuffix(p, "_") { 41 | result = append(result, p+"_") 42 | continue 43 | } 44 | 45 | result = append(result, p) 46 | } 47 | 48 | return result 49 | } 50 | -------------------------------------------------------------------------------- /go_config/source/file/watcher.go: -------------------------------------------------------------------------------- 1 | //+build !linux 2 | 3 | package file 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/fsnotify/fsnotify" 9 | "github.com/whatvn/denny/go_config/source" 10 | ) 11 | 12 | type watcher struct { 13 | f *file 14 | 15 | fw *fsnotify.Watcher 16 | exit chan bool 17 | } 18 | 19 | func newWatcher(f *file) (source.Watcher, error) { 20 | fw, err := fsnotify.NewWatcher() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | fw.Add(f.path) 26 | 27 | return &watcher{ 28 | f: f, 29 | fw: fw, 30 | exit: make(chan bool), 31 | }, nil 32 | } 33 | 34 | func (w *watcher) Next() (*source.ChangeSet, error) { 35 | // is it closed? 36 | select { 37 | case <-w.exit: 38 | return nil, source.ErrWatcherStopped 39 | default: 40 | } 41 | 42 | // try get the event 43 | select { 44 | case event, _ := <-w.fw.Events: 45 | if event.Op == fsnotify.Rename { 46 | // check existence of file, and add watch again 47 | _, err := os.Stat(event.Name) 48 | if err == nil || os.IsExist(err) { 49 | w.fw.Add(event.Name) 50 | } 51 | } 52 | 53 | c, err := w.f.Read() 54 | if err != nil { 55 | return nil, err 56 | } 57 | return c, nil 58 | case err := <-w.fw.Errors: 59 | return nil, err 60 | case <-w.exit: 61 | return nil, source.ErrWatcherStopped 62 | } 63 | } 64 | 65 | func (w *watcher) Stop() error { 66 | return w.fw.Close() 67 | } 68 | -------------------------------------------------------------------------------- /middleware/grpc/logger.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/whatvn/denny/log" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/grpc/peer" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | func LoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 15 | var ( 16 | logger = log.New(&log.JSONFormatter{}) 17 | start = time.Now() 18 | panicking = true 19 | ) 20 | 21 | p, ok := peer.FromContext(ctx) 22 | if ok { 23 | logger.WithField("request_ip", p.Addr.String()) 24 | } 25 | logger.WithFields(map[string]interface{}{ 26 | "start": start, 27 | "uri": info.FullMethod, 28 | "request": logger.ToJsonString(req), 29 | }) 30 | 31 | defer func() { 32 | var ( 33 | code = codes.OK 34 | end = time.Now() 35 | ) 36 | 37 | switch { 38 | case err != nil: 39 | code = status.Code(err) 40 | case panicking: 41 | code = codes.Internal 42 | } 43 | logger.WithField("code", code.String()) 44 | logger.Infof("latency: %d", end.Sub(start).Milliseconds()) 45 | 46 | }() 47 | 48 | ctx = context.WithValue(ctx, log.LogKey, logger) 49 | resp, err = handler(ctx, req) 50 | panicking = false // normal exit, no panic happened, disarms defer 51 | logger.WithField("response", resp) 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /go_config/source/file/file.go: -------------------------------------------------------------------------------- 1 | // Package file is a file source. Expected format is json 2 | package file 3 | 4 | import ( 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/whatvn/denny/go_config/source" 9 | ) 10 | 11 | type file struct { 12 | path string 13 | opts source.Options 14 | } 15 | 16 | var ( 17 | DefaultPath = "config.json" 18 | ) 19 | 20 | func (f *file) Read() (*source.ChangeSet, error) { 21 | fh, err := os.Open(f.path) 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer fh.Close() 26 | b, err := ioutil.ReadAll(fh) 27 | if err != nil { 28 | return nil, err 29 | } 30 | info, err := fh.Stat() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | cs := &source.ChangeSet{ 36 | Format: format(f.path, f.opts.Encoder), 37 | Source: f.String(), 38 | Timestamp: info.ModTime(), 39 | Data: b, 40 | } 41 | cs.Checksum = cs.Sum() 42 | 43 | return cs, nil 44 | } 45 | 46 | func (f *file) String() string { 47 | return "file" 48 | } 49 | 50 | func (f *file) Watch() (source.Watcher, error) { 51 | if _, err := os.Stat(f.path); err != nil { 52 | return nil, err 53 | } 54 | return newWatcher(f) 55 | } 56 | 57 | func NewSource(opts ...source.Option) source.Source { 58 | options := source.NewOptions(opts...) 59 | path := DefaultPath 60 | f, ok := options.Context.Value(filePathKey{}).(string) 61 | if ok { 62 | path = f 63 | } 64 | return &file{opts: options, path: path} 65 | } 66 | -------------------------------------------------------------------------------- /example/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/whatvn/denny/config" 6 | "github.com/whatvn/denny/log" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | ) 11 | 12 | func configFile() (*os.File, error) { 13 | data := []byte(`{"foo": "bar", "dennyObj": {"sister": "jenny"}}`) 14 | path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano())) 15 | fh, err := os.Create(path) 16 | if err != nil { 17 | return nil, err 18 | } 19 | _, err = fh.Write(data) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return fh, nil 25 | } 26 | 27 | type Denny struct { 28 | Age int 29 | Sister string 30 | } 31 | 32 | var ( 33 | dennyObj = &Denny{} 34 | logger = log.New() 35 | ) 36 | 37 | func load() { 38 | config.Scan(dennyObj, "dennyObj") 39 | } 40 | 41 | func main() { 42 | f, err := configFile() 43 | if err != nil { 44 | logger.Infof("error %v", err) 45 | } 46 | // read config from file 47 | config.New(f.Name()) 48 | 49 | config.WithEtcd( 50 | config.WithEtcdAddress("10.109.3.93:7379"), 51 | config.WithPath("/acquiringcore/ae/config"), 52 | ) 53 | 54 | load() 55 | logger.Println(dennyObj.Age) 56 | logger.Println(dennyObj.Sister) 57 | w, _ := config.Watch() 58 | 59 | for { 60 | 61 | _, err := w.Next() 62 | if err != nil { 63 | logger.Error(err) 64 | } 65 | load() 66 | logger.Println(dennyObj.Age) 67 | logger.Println(dennyObj.Sister) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /go_config/source/file/watcher_linux.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package file 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/fsnotify/fsnotify" 9 | "github.com/whatvn/denny/go_config/source" 10 | ) 11 | 12 | type watcher struct { 13 | f *file 14 | 15 | fw *fsnotify.Watcher 16 | exit chan bool 17 | } 18 | 19 | func newWatcher(f *file) (source.Watcher, error) { 20 | fw, err := fsnotify.NewWatcher() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | fw.Add(f.path) 26 | 27 | return &watcher{ 28 | f: f, 29 | fw: fw, 30 | exit: make(chan bool), 31 | }, nil 32 | } 33 | 34 | func (w *watcher) Next() (*source.ChangeSet, error) { 35 | // is it closed? 36 | select { 37 | case <-w.exit: 38 | return nil, source.ErrWatcherStopped 39 | default: 40 | } 41 | 42 | // try get the event 43 | select { 44 | case event, _ := <-w.fw.Events: 45 | if event.Op == fsnotify.Rename { 46 | // check existence of file, and add watch again 47 | _, err := os.Stat(event.Name) 48 | if err == nil || os.IsExist(err) { 49 | w.fw.Add(event.Name) 50 | } 51 | } 52 | 53 | c, err := w.f.Read() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // add path again for the event bug of fsnotify 59 | w.fw.Add(w.f.path) 60 | 61 | return c, nil 62 | case err := <-w.fw.Errors: 63 | return nil, err 64 | case <-w.exit: 65 | return nil, source.ErrWatcherStopped 66 | } 67 | } 68 | 69 | func (w *watcher) Stop() error { 70 | return w.fw.Close() 71 | } 72 | -------------------------------------------------------------------------------- /middleware/http/logger.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/whatvn/denny" 7 | "github.com/whatvn/denny/log" 8 | "time" 9 | ) 10 | 11 | func Logger() denny.HandleFunc { 12 | return func(ctx *denny.Context) { 13 | logger := log.New(&log.JSONFormatter{}) 14 | var ( 15 | clientIP = ctx.ClientIP() 16 | method = ctx.Request.Method 17 | 18 | userAgent = ctx.Request.UserAgent() 19 | uri = ctx.Request.URL 20 | errs string 21 | start = time.Now() 22 | isError bool 23 | ) 24 | 25 | logger.WithFields(map[string]interface{}{ 26 | "client_ip": clientIP, 27 | "request_method": method, 28 | "user_agent": userAgent, 29 | "uri": uri, 30 | }) 31 | ctx.Request = ctx.Request.WithContext(context.WithValue(ctx, log.LogKey, logger)) 32 | ctx.Set(log.LogKey, logger) 33 | ctx.Next() 34 | var ( 35 | statusCode = ctx.Writer.Status() 36 | ) 37 | logger.WithField("Status", statusCode) 38 | if ctx.Errors != nil { 39 | isError = true 40 | bs, err := ctx.Errors.MarshalJSON() 41 | if err == nil { 42 | errs = string(bs) 43 | } else { 44 | errs = ctx.Errors.String() 45 | } 46 | } 47 | if len(errs) > 0 { 48 | logger.WithField("Errors", errs) 49 | } 50 | end := time.Now() 51 | logger.WithField("end", end) 52 | msg := fmt.Sprintf("latency: %v", end.Sub(start)) 53 | if isError { 54 | logger.Error(msg) 55 | } else { 56 | logger.Info(msg) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, hungnv. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Gengo, Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /go_config/loader/loader.go: -------------------------------------------------------------------------------- 1 | // package loader manages loading from multiple sources 2 | package loader 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/whatvn/denny/go_config/reader" 8 | "github.com/whatvn/denny/go_config/source" 9 | ) 10 | 11 | // Loader manages loading sources 12 | type Loader interface { 13 | // Stop the loader 14 | Close() error 15 | // Load the sources 16 | Load(...source.Source) error 17 | // A Snapshot of loaded config 18 | Snapshot() (*Snapshot, error) 19 | // Force sync of sources 20 | Sync() error 21 | // Watch for changes 22 | Watch(...string) (Watcher, error) 23 | // Name of loader 24 | String() string 25 | } 26 | 27 | // Watcher lets you watch sources and returns a merged ChangeSet 28 | type Watcher interface { 29 | // First call to next may return the current Snapshot 30 | // If you are watching a path then only the data from 31 | // that path is returned. 32 | Next() (*Snapshot, error) 33 | // Stop watching for changes 34 | Stop() error 35 | } 36 | 37 | // Snapshot is a merged ChangeSet 38 | type Snapshot struct { 39 | // The merged ChangeSet 40 | ChangeSet *source.ChangeSet 41 | // Deterministic and comparable version of the snapshot 42 | Version string 43 | } 44 | 45 | type Options struct { 46 | Reader reader.Reader 47 | Source []source.Source 48 | 49 | // for alternative data 50 | Context context.Context 51 | } 52 | 53 | type Option func(o *Options) 54 | 55 | // Copy snapshot 56 | func Copy(s *Snapshot) *Snapshot { 57 | cs := *(s.ChangeSet) 58 | 59 | return &Snapshot{ 60 | ChangeSet: &cs, 61 | Version: s.Version, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /go_config/source/file/README.md: -------------------------------------------------------------------------------- 1 | # File Source 2 | 3 | The file source reads config from a file. 4 | 5 | It uses the File extension to determine the Format e.g `config.yaml` has the yaml format. 6 | It does not make use of encoders or interpet the file data. If a file extension is not present 7 | the source Format will default to the Encoder in options. 8 | 9 | ## Example 10 | 11 | A config file format in json 12 | 13 | ```json 14 | { 15 | "hosts": { 16 | "database": { 17 | "address": "10.0.0.1", 18 | "port": 3306 19 | }, 20 | "cache": { 21 | "address": "10.0.0.2", 22 | "port": 6379 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | ## New Source 29 | 30 | Specify file source with path to file. Path is optional and will default to `config.json` 31 | 32 | ```go 33 | fileSource := file.NewSource( 34 | file.WithPath("/tmp/config.json"), 35 | ) 36 | ``` 37 | 38 | ## File Format 39 | 40 | To load different file formats e.g yaml, toml, xml simply specify them with their extension 41 | 42 | ``` 43 | fileSource := file.NewSource( 44 | file.WithPath("/tmp/config.yaml"), 45 | ) 46 | ``` 47 | 48 | If you want to specify a file without extension, ensure you set the encoder to the same format 49 | 50 | ``` 51 | e := toml.NewEncoder() 52 | 53 | fileSource := file.NewSource( 54 | file.WithPath("/tmp/config"), 55 | source.WithEncoder(e), 56 | ) 57 | ``` 58 | 59 | ## Load Source 60 | 61 | Load the source into config 62 | 63 | ```go 64 | // Create new config 65 | conf := config.NewConfig() 66 | 67 | // Load file source 68 | conf.Load(fileSource) 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /naming/redis/naming.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "github.com/whatvn/denny/naming" 5 | "time" 6 | ) 7 | 8 | func (r *redis) Register(addr string, ttl int) error { 9 | var ( 10 | ticker = time.NewTicker(time.Second * time.Duration(ttl)) 11 | err error 12 | svcPath = "/" + naming.Prefix + "/" + r.serviceName + "/" + addr 13 | ) 14 | 15 | r.Infof("register %s with registy", svcPath) 16 | err = r.register(addr, ttl) 17 | if err != nil { 18 | r.Errorf("error %v", err) 19 | return err 20 | } 21 | 22 | go func() { 23 | for { 24 | select { 25 | case _ = <-ticker.C: 26 | _ = r.register(addr, ttl) 27 | case _ = <-r.shutdown: 28 | // receive message from shutdown channel 29 | // will stop current thread and stop ticker to prevent thread leak 30 | ticker.Stop() 31 | return 32 | } 33 | } 34 | }() 35 | 36 | return nil 37 | } 38 | 39 | func (r *redis) register(addr string, ttl int) error { 40 | var ( 41 | svcPath = "/" + naming.Prefix + "/" + r.serviceName + "/" + addr 42 | ) 43 | 44 | existCmd := r.cli.Exists(svcPath) 45 | val, err := existCmd.Result() 46 | 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if val != 0 { 52 | // increase expired time 53 | touchCmd := r.cli.Expire(svcPath, time.Duration(ttl*2)*time.Second) 54 | return touchCmd.Err() 55 | } 56 | setCmd := r.cli.Set(svcPath, addr, time.Duration(ttl*2)*time.Second) 57 | return setCmd.Err() 58 | } 59 | 60 | func (r *redis) UnRegister(addr string) error { 61 | var ( 62 | svcPath = "/" + naming.Prefix + "/" + r.serviceName + "/" + addr 63 | ) 64 | r.shutdown <- "stop" 65 | return r.cli.Del(svcPath).Err() 66 | } 67 | -------------------------------------------------------------------------------- /example/handler1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/opentracing/opentracing-go" 5 | "github.com/uber/jaeger-client-go" 6 | zk "github.com/uber/jaeger-client-go/transport/zipkin" 7 | "github.com/uber/jaeger-client-go/zipkin" 8 | "github.com/whatvn/denny/middleware/http" 9 | "github.com/whatvn/denny/middleware/http/ot" 10 | 11 | "github.com/whatvn/denny" 12 | ) 13 | 14 | type zController struct { 15 | denny.Controller 16 | } 17 | 18 | func (z zController) Handle(ctx *denny.Context) { 19 | z.AddLog("receive request") 20 | var str = "hello" 21 | z.AddLog("do more thing") 22 | str += " denny" 23 | z.Infof("finished") 24 | ctx.Writer.Write([]byte(str)) 25 | } 26 | 27 | func reporter() jaeger.Transport { 28 | transport, err := zk.NewHTTPTransport( 29 | "http://10.109.3.93:9411/api/v1/spans", 30 | zk.HTTPBatchSize(10), 31 | zk.HTTPLogger(jaeger.StdLogger), 32 | ) 33 | if err != nil { 34 | panic(err) 35 | } 36 | return transport 37 | } 38 | 39 | func main() { 40 | server := denny.NewServer() 41 | propagator := zipkin.NewZipkinB3HTTPHeaderPropagator() 42 | trace, closer := jaeger.NewTracer( 43 | "server-1", 44 | jaeger.NewConstSampler(true), 45 | jaeger.NewRemoteReporter(reporter()), 46 | jaeger.TracerOptions.Injector(opentracing.HTTPHeaders, propagator), 47 | jaeger.TracerOptions.Extractor(opentracing.HTTPHeaders, propagator), 48 | jaeger.TracerOptions.ZipkinSharedRPCSpan(true), 49 | ) 50 | defer closer.Close() 51 | opentracing.SetGlobalTracer(trace) 52 | 53 | server.Use(http.Logger()).Use(ot.RequestTracer()) 54 | server.Controller("/", denny.HttpGet, &zController{}) 55 | server.GraceFulStart(":8081") 56 | } 57 | -------------------------------------------------------------------------------- /middleware/http/ot/options.go: -------------------------------------------------------------------------------- 1 | package ot 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/opentracing/opentracing-go" 6 | ) 7 | 8 | type OptionFunc func(*options) 9 | 10 | func SetOperationNameFn(fn func(*gin.Context) string) OptionFunc { 11 | return func(opts *options) { 12 | opts.operationNameFn = fn 13 | } 14 | } 15 | 16 | func SetErrorFn(fn func(*gin.Context) bool) OptionFunc { 17 | return func(opts *options) { 18 | opts.errorFn = fn 19 | } 20 | } 21 | 22 | func SetResourceNameFn(fn func(*gin.Context) string) OptionFunc { 23 | return func(opts *options) { 24 | opts.resourceNameFn = fn 25 | } 26 | } 27 | 28 | func SetBeforeHook(fn func(opentracing.Span, *gin.Context)) OptionFunc { 29 | return func(opts *options) { 30 | opts.beforeHook = fn 31 | } 32 | } 33 | 34 | func SetAfterHook(fn func(opentracing.Span, *gin.Context)) OptionFunc { 35 | return func(opts *options) { 36 | opts.afterHook = fn 37 | } 38 | } 39 | 40 | func (opts *options) handleDefaultOptions() { 41 | if opts.operationNameFn == nil { 42 | opts.operationNameFn = func(ctx *gin.Context) string { 43 | return "gin.request" 44 | } 45 | } 46 | 47 | if opts.errorFn == nil { 48 | opts.errorFn = func(ctx *gin.Context) bool { 49 | return ctx.Writer.Status() >= 400 || len(ctx.Errors) > 0 50 | } 51 | } 52 | 53 | if opts.resourceNameFn == nil { 54 | opts.resourceNameFn = func(ctx *gin.Context) string { 55 | return ctx.HandlerName() 56 | } 57 | } 58 | 59 | if opts.beforeHook == nil { 60 | opts.beforeHook = func(span opentracing.Span, ctx *gin.Context) { 61 | return 62 | } 63 | } 64 | 65 | if opts.afterHook == nil { 66 | opts.afterHook = func(span opentracing.Span, ctx *gin.Context) { 67 | return 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /go_config/source/flag/flag_test.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "testing" 7 | ) 8 | 9 | var ( 10 | dbuser = flag.String("database-user", "default", "db user") 11 | dbhost = flag.String("database-host", "", "db host") 12 | dbpw = flag.String("database-password", "", "db pw") 13 | ) 14 | 15 | func initTestFlags() { 16 | flag.Set("database-host", "localhost") 17 | flag.Set("database-password", "some-password") 18 | flag.Parse() 19 | } 20 | 21 | func TestFlagsrc_Read(t *testing.T) { 22 | initTestFlags() 23 | source := NewSource() 24 | c, err := source.Read() 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | 29 | var actual map[string]interface{} 30 | if err := json.Unmarshal(c.Data, &actual); err != nil { 31 | t.Error(err) 32 | } 33 | 34 | actualDB := actual["database"].(map[string]interface{}) 35 | if actualDB["host"] != *dbhost { 36 | t.Errorf("expected %v got %v", *dbhost, actualDB["host"]) 37 | } 38 | 39 | if actualDB["password"] != *dbpw { 40 | t.Errorf("expected %v got %v", *dbpw, actualDB["password"]) 41 | } 42 | 43 | // unset flags should not be loaded 44 | if actualDB["user"] != nil { 45 | t.Errorf("expected %v got %v", nil, actualDB["user"]) 46 | } 47 | } 48 | 49 | func TestFlagsrc_ReadAll(t *testing.T) { 50 | initTestFlags() 51 | source := NewSource(IncludeUnset(true)) 52 | c, err := source.Read() 53 | if err != nil { 54 | t.Error(err) 55 | } 56 | 57 | var actual map[string]interface{} 58 | if err := json.Unmarshal(c.Data, &actual); err != nil { 59 | t.Error(err) 60 | } 61 | 62 | actualDB := actual["database"].(map[string]interface{}) 63 | 64 | // unset flag defaults should be loaded 65 | if actualDB["user"] != *dbuser { 66 | t.Errorf("expected %v got %v", *dbuser, actualDB["user"]) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /go_config/options/options.go: -------------------------------------------------------------------------------- 1 | // Package options provides a way to initialise options 2 | package options 3 | 4 | import ( 5 | "sync" 6 | ) 7 | 8 | // Options is used for initialisation 9 | type Options interface { 10 | // Initialise options 11 | Init(...Option) error 12 | // Options returns the current options 13 | Values() *Values 14 | // The name for who these options exist 15 | String() string 16 | } 17 | 18 | // Values holds the set of option values and protects them 19 | type Values struct { 20 | sync.RWMutex 21 | values map[interface{}]interface{} 22 | } 23 | 24 | // Option gives access to options 25 | type Option func(o *Values) error 26 | 27 | // Get a value from options 28 | func (o *Values) Get(k interface{}) (interface{}, bool) { 29 | o.RLock() 30 | defer o.RUnlock() 31 | v, ok := o.values[k] 32 | return v, ok 33 | } 34 | 35 | // Set a value in the options 36 | func (o *Values) Set(k, v interface{}) error { 37 | o.Lock() 38 | defer o.Unlock() 39 | if o.values == nil { 40 | o.values = map[interface{}]interface{}{} 41 | } 42 | o.values[k] = v 43 | return nil 44 | } 45 | 46 | // SetOption executes an option 47 | func (o *Values) Option(op Option) error { 48 | return op(o) 49 | } 50 | 51 | // WithValue allows you to set any value within the options 52 | func WithValue(k, v interface{}) Option { 53 | return func(o *Values) error { 54 | return o.Set(k, v) 55 | } 56 | } 57 | 58 | // WithOption gives you the ability to create an option that accesses values 59 | func WithOption(o Option) Option { 60 | return o 61 | } 62 | 63 | // String sets the string 64 | func WithString(s string) Option { 65 | return WithValue(stringKey{}, s) 66 | } 67 | 68 | // NewOptions returns a new initialiser 69 | func NewOptions(opts ...Option) Options { 70 | o := new(defaultOptions) 71 | o.Init(opts...) 72 | return o 73 | } 74 | -------------------------------------------------------------------------------- /naming/registry.go: -------------------------------------------------------------------------------- 1 | // package naming is interface for both registry register/unregister and grpc builder/resolver 2 | package naming 3 | 4 | import ( 5 | "google.golang.org/grpc" 6 | "google.golang.org/grpc/resolver" 7 | ) 8 | 9 | const ( 10 | // PREFIX uses here to differentiate between denny etcd prefix and other service prefix 11 | // in etcd directory/files 12 | Prefix = "_DENNY_" 13 | ) 14 | 15 | // Registry is based interface, which is composed of grpc resolver.Builder, resolver.Resolver and also 16 | // contains method to register and unregister from naming storage 17 | type Registry interface { 18 | Register(addr string, ttl int) error 19 | UnRegister(addr string) error 20 | Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) 21 | Scheme() string 22 | SvcName() string 23 | } 24 | 25 | const defaultBalancingPolicy = `{"loadBalancingPolicy":"round_robin"}` 26 | 27 | // DefaultBalancePolicy returns default grpc service config 28 | // which required by new grpc API so client does not have to supply 29 | // json config everytime 30 | func DefaultBalancePolicy() grpc.DialOption { 31 | return grpc.WithDefaultServiceConfig(defaultBalancingPolicy) 32 | } 33 | 34 | // Exist checks if given addr is already exist in grpc resolver address list 35 | func Exist(l []resolver.Address, addr string) bool { 36 | for i := range l { 37 | if l[i].Addr == addr { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | // Remove removes an address from grpc resolver address list (because it's no longer available in naming registry) 45 | func Remove(s []resolver.Address, addr string) ([]resolver.Address, bool) { 46 | for i := range s { 47 | if s[i].Addr == addr { 48 | s[i] = s[len(s)-1] 49 | return s[:len(s)-1], true 50 | } 51 | } 52 | return nil, false 53 | } 54 | -------------------------------------------------------------------------------- /naming/etcd/etcd.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "errors" 5 | "github.com/whatvn/denny/log" 6 | "github.com/whatvn/denny/naming" 7 | "go.etcd.io/etcd/clientv3" 8 | "google.golang.org/grpc/resolver" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type etcd struct { 14 | cli *clientv3.Client 15 | *log.Log 16 | shutdown chan interface{} 17 | cc resolver.ClientConn 18 | serviceName string 19 | } 20 | 21 | // New etcd 22 | // implement github.com/whatvn/denny/naming#Registry 23 | // with 2 methods: Register and UnRegister 24 | func New(etcdAddrs, serviceName string) naming.Registry { 25 | 26 | cli, err := clientv3.New(clientv3.Config{ 27 | Endpoints: strings.Split(etcdAddrs, ";"), 28 | DialTimeout: 15 * time.Second, 29 | }) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | if len(serviceName) == 0 { 35 | panic(errors.New("invalid service name")) 36 | } 37 | registry := &etcd{ 38 | cli: cli, 39 | Log: log.New(), 40 | serviceName: serviceName, 41 | shutdown: make(chan interface{}, 1), 42 | } 43 | registry.WithField("etcd", etcdAddrs) 44 | return registry 45 | } 46 | 47 | // NewWithClientConfig etcd 48 | // implement github.com/whatvn/denny/naming#Registry 49 | // with 2 methods: Register and UnRegister 50 | func NewWithClientConfig(serviceName string, etcdClientCfg clientv3.Config) naming.Registry { 51 | 52 | cli, err := clientv3.New(etcdClientCfg) 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | if len(serviceName) == 0 { 58 | panic(errors.New("invalid service name")) 59 | } 60 | registry := &etcd{ 61 | cli: cli, 62 | Log: log.New(), 63 | serviceName: serviceName, 64 | shutdown: make(chan interface{}, 1), 65 | } 66 | registry.WithField("etcd", etcdClientCfg.Endpoints) 67 | return registry 68 | } 69 | 70 | var _ naming.Registry = new(etcd) 71 | -------------------------------------------------------------------------------- /go_config/source/env/README.md: -------------------------------------------------------------------------------- 1 | # Env Source 2 | 3 | The env source reads config from environment variables 4 | 5 | ## Format 6 | 7 | We expect environment variables to be in the standard format of FOO=bar 8 | 9 | Keys are converted to lowercase and split on underscore. 10 | 11 | 12 | ### Example 13 | 14 | ``` 15 | DATABASE_ADDRESS=127.0.0.1 16 | DATABASE_PORT=3306 17 | ``` 18 | 19 | Becomes 20 | 21 | ```json 22 | { 23 | "database": { 24 | "address": "127.0.0.1", 25 | "port": 3306 26 | } 27 | } 28 | ``` 29 | 30 | ## Prefixes 31 | 32 | Environment variables can be namespaced so we only have access to a subset. Two options are available: 33 | 34 | ``` 35 | WithPrefix(p ...string) 36 | WithStrippedPrefix(p ...string) 37 | ``` 38 | 39 | The former will preserve the prefix and make it a top level key in the config. The latter eliminates the prefix, reducing the nesting by one. 40 | 41 | #### Example: 42 | 43 | Given ENVs of: 44 | 45 | ``` 46 | APP_DATABASE_ADDRESS=127.0.0.1 47 | APP_DATABASE_PORT=3306 48 | VAULT_ADDR=vault:1337 49 | ``` 50 | 51 | and a source initialized as follows: 52 | 53 | ``` 54 | src := env.NewSource( 55 | env.WithPrefix("VAULT"), 56 | env.WithStrippedPrefix("APP"), 57 | ) 58 | ``` 59 | 60 | The resulting config will be: 61 | 62 | ``` 63 | { 64 | "database": { 65 | "address": "127.0.0.1", 66 | "port": 3306 67 | }, 68 | "vault": { 69 | "addr": "vault:1337" 70 | } 71 | } 72 | ``` 73 | 74 | 75 | ## New Source 76 | 77 | Specify source with data 78 | 79 | ```go 80 | src := env.NewSource( 81 | // optionally specify prefix 82 | env.WithPrefix("MICRO"), 83 | ) 84 | ``` 85 | 86 | ## Load Source 87 | 88 | Load the source into config 89 | 90 | ```go 91 | // Create new config 92 | conf := config.NewConfig() 93 | 94 | // Load env source 95 | conf.Load(src) 96 | ``` 97 | -------------------------------------------------------------------------------- /go_config/reader/json/values_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/whatvn/denny/go_config/source" 8 | ) 9 | 10 | func TestValues(t *testing.T) { 11 | emptyStr := "" 12 | testData := []struct { 13 | csdata []byte 14 | path []string 15 | accepter interface{} 16 | value interface{} 17 | }{ 18 | { 19 | []byte(`{"foo": "bar", "baz": {"bar": "cat"}}`), 20 | []string{"foo"}, 21 | emptyStr, 22 | "bar", 23 | }, 24 | { 25 | []byte(`{"foo": "bar", "baz": {"bar": "cat"}}`), 26 | []string{"baz", "bar"}, 27 | emptyStr, 28 | "cat", 29 | }, 30 | } 31 | 32 | for idx, test := range testData { 33 | values, err := newValues(&source.ChangeSet{ 34 | Data: test.csdata, 35 | }) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | err = values.Get(test.path...).Scan(&test.accepter) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | if test.accepter != test.value { 45 | t.Fatalf("No.%d Expected %v got %v for path %v", idx, test.value, test.accepter, test.path) 46 | } 47 | } 48 | } 49 | 50 | func TestStructArray(t *testing.T) { 51 | type T struct { 52 | Foo string 53 | } 54 | 55 | emptyTSlice := []T{} 56 | 57 | testData := []struct { 58 | csdata []byte 59 | accepter []T 60 | value []T 61 | }{ 62 | { 63 | []byte(`[{"foo": "bar"}]`), 64 | emptyTSlice, 65 | []T{T{Foo: "bar"}}, 66 | }, 67 | } 68 | 69 | for idx, test := range testData { 70 | values, err := newValues(&source.ChangeSet{ 71 | Data: test.csdata, 72 | }) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | err = values.Get().Scan(&test.accepter) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | if !reflect.DeepEqual(test.accepter, test.value) { 82 | t.Fatalf("No.%d Expected %v got %v", idx, test.value, test.accepter) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /go_config/source/etcd/watcher.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "time" 8 | 9 | "github.com/whatvn/denny/go_config/source" 10 | cetcd "go.etcd.io/etcd/clientv3" 11 | ) 12 | 13 | type watcher struct { 14 | opts source.Options 15 | name string 16 | 17 | sync.RWMutex 18 | cs *source.ChangeSet 19 | 20 | ch chan *source.ChangeSet 21 | exit chan bool 22 | } 23 | 24 | func newWatcher(key string, wc cetcd.Watcher, cs *source.ChangeSet, opts source.Options) (source.Watcher, error) { 25 | w := &watcher{ 26 | opts: opts, 27 | name: "etcd", 28 | cs: cs, 29 | ch: make(chan *source.ChangeSet), 30 | exit: make(chan bool), 31 | } 32 | 33 | ch := wc.Watch(context.Background(), key) 34 | 35 | go w.run(wc, ch) 36 | 37 | return w, nil 38 | } 39 | 40 | func (w *watcher) handle(ev *cetcd.Event) { 41 | b := ev.Kv.Value 42 | // create new changeset 43 | cs := &source.ChangeSet{ 44 | Timestamp: time.Now(), 45 | Source: w.name, 46 | Data: b, 47 | Format: w.opts.Encoder.String(), 48 | } 49 | cs.Checksum = cs.Sum() 50 | 51 | // set base change set 52 | w.Lock() 53 | w.cs = cs 54 | w.Unlock() 55 | // send update 56 | w.ch <- cs 57 | } 58 | 59 | func (w *watcher) run(wc cetcd.Watcher, ch cetcd.WatchChan) { 60 | for { 61 | select { 62 | case rsp, ok := <-ch: 63 | if !ok { 64 | return 65 | } 66 | for _, ev := range rsp.Events { 67 | w.handle(ev) 68 | } 69 | case <-w.exit: 70 | _ = wc.Close() 71 | return 72 | } 73 | } 74 | } 75 | 76 | func (w *watcher) Next() (*source.ChangeSet, error) { 77 | select { 78 | case cs := <-w.ch: 79 | return cs, nil 80 | case <-w.exit: 81 | return nil, errors.New("watcher stopped") 82 | } 83 | } 84 | 85 | func (w *watcher) Stop() error { 86 | select { 87 | case <-w.exit: 88 | return nil 89 | default: 90 | close(w.exit) 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /middleware/http/ot/middleware.go: -------------------------------------------------------------------------------- 1 | package ot 2 | 3 | import ( 4 | "context" 5 | "github.com/gin-gonic/gin" 6 | "github.com/opentracing/opentracing-go" 7 | "github.com/opentracing/opentracing-go/ext" 8 | ) 9 | 10 | type options struct { 11 | beforeHook func(opentracing.Span, *gin.Context) 12 | afterHook func(opentracing.Span, *gin.Context) 13 | operationNameFn func(*gin.Context) string 14 | errorFn func(*gin.Context) bool 15 | resourceNameFn func(*gin.Context) string 16 | } 17 | 18 | func RequestTracer(opts ...OptionFunc) gin.HandlerFunc { 19 | mwOptions := &options{} 20 | for _, opt := range opts { 21 | opt(mwOptions) 22 | } 23 | 24 | mwOptions.handleDefaultOptions() 25 | 26 | return func(c *gin.Context) { 27 | var ( 28 | span opentracing.Span = opentracing.StartSpan(c.FullPath()) 29 | tracer = opentracing.GlobalTracer() 30 | carrier = opentracing.HTTPHeadersCarrier(c.Request.Header) 31 | ) 32 | ctx, err := tracer.Extract(opentracing.HTTPHeaders, carrier) 33 | 34 | if err == nil { 35 | span = opentracing.StartSpan(c.FullPath(), opentracing.ChildOf(ctx)) 36 | } 37 | 38 | ext.HTTPMethod.Set(span, c.Request.Method) 39 | ext.HTTPUrl.Set(span, c.Request.URL.String()) 40 | span.SetTag("resource.name", mwOptions.resourceNameFn(c)) 41 | c.Request = c.Request.WithContext(opentracing.ContextWithSpan(c.Request.Context(), span)) 42 | mwOptions.beforeHook(span, c) 43 | c.Next() 44 | mwOptions.afterHook(span, c) 45 | ext.Error.Set(span, mwOptions.errorFn(c)) 46 | ext.HTTPStatusCode.Set(span, uint16(c.Writer.Status())) 47 | span.SetTag("handler.error", c.Errors.JSON()) 48 | span.Finish() 49 | } 50 | } 51 | 52 | func GetSpan(c context.Context) (opentracing.Span, bool) { 53 | var span = opentracing.SpanFromContext(c) 54 | if span != nil { 55 | return span, true 56 | } 57 | return nil, false 58 | } 59 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/sirupsen/logrus" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | const ( 12 | LogKey = "DennyLogger" 13 | ) 14 | 15 | type Log struct { 16 | *logrus.Entry 17 | sync.Mutex 18 | step int32 19 | } 20 | 21 | type TextFormatter = logrus.TextFormatter 22 | type JSONFormatter = logrus.JSONFormatter 23 | type Formatter = logrus.Formatter 24 | 25 | // New return a new log object with log start time 26 | func New(formatter ...Formatter) *Log { 27 | if len(formatter) > 0 { 28 | logrus.SetFormatter(formatter[0]) 29 | } 30 | logTime := time.Now().Format(time.RFC3339) 31 | return &Log{ 32 | Entry: logrus.WithField("Time", logTime), 33 | } 34 | } 35 | 36 | // ToJsonString convert an object into json string to beautify log 37 | // return nil if marshalling error 38 | func (l *Log) ToJsonString(input interface{}) string { 39 | if bytes, err := json.Marshal(input); err == nil { 40 | return string(bytes) 41 | } 42 | return "" 43 | } 44 | 45 | func (l *Log) addStep() int32 { 46 | l.Lock() 47 | defer l.Unlock() 48 | l.step += 1 49 | return l.step 50 | } 51 | 52 | // AddLog add a new field to log with step = current step + 1 53 | func (l *Log) AddLog(line string, format ...interface{}) *Log { 54 | step := fmt.Sprintf("STEP_%d", l.addStep()) 55 | if len(format) > 0 { 56 | logLine := fmt.Sprintf(line, format...) 57 | l.Entry = l.Entry.WithField(step, logLine) 58 | return l 59 | } 60 | l.Entry = l.Entry.WithField(step, line) 61 | return l 62 | } 63 | 64 | // WithField a a new key = value to log with key = field, value = value 65 | func (l *Log) WithField(field string, value interface{}) *Log { 66 | l.Entry = l.Entry.WithField(field, value) 67 | return l 68 | } 69 | 70 | // WithFields add multiple key/value to log: key1 = value1, key2 = value2 71 | func (l *Log) WithFields(fields map[string]interface{}) *Log { 72 | l.Entry = l.Entry.WithFields(fields) 73 | return l 74 | } 75 | -------------------------------------------------------------------------------- /go_config/README.md: -------------------------------------------------------------------------------- 1 | # Config [![GoDoc](https://godoc.org/github.com/micro/go-micro/config?status.svg)](https://godoc.org/github.com/micro/go-micro/config) 2 | 3 | 4 | ## Fork go-config into denny to make its more portable 5 | Config is a pluggable dynamic config package 6 | 7 | Most config in applications are statically configured or include complex logic to load from multiple sources. 8 | Go Config makes this easy, pluggable and mergeable. You'll never have to deal with config in the same way again. 9 | 10 | ## Features 11 | 12 | - **Dynamic Loading** - Load configuration from multiple source as and when needed. Go Config manages watching config sources 13 | in the background and automatically merges and updates an in memory view. 14 | 15 | - **Pluggable Sources** - Choose from any number of sources to load and merge config. The backend source is abstracted away into 16 | a standard format consumed internally and decoded via encoders. Sources can be env vars, flags, file, etcd, k8s configmap, etc. 17 | 18 | - **Mergeable Config** - If you specify multiple sources of config, regardless of format, they will be merged and presented in 19 | a single view. This massively simplifies priority order loading and changes based on environment. 20 | 21 | - **Observe Changes** - Optionally watch the config for changes to specific values. Hot reload your app using Go Config's watcher. 22 | You don't have to handle ad-hoc hup reloading or whatever else, just keep reading the config and watch for changes if you need 23 | to be notified. 24 | 25 | - **Sane Defaults** - In case config loads badly or is completely wiped away for some unknown reason, you can specify fallback 26 | values when accessing any config values directly. This ensures you'll always be reading some sane default in the event of a problem. 27 | 28 | ## Getting Started 29 | 30 | For detailed information or architecture, installation and general usage see the [docs](https://micro.mu/docs/go-config.html) 31 | 32 | -------------------------------------------------------------------------------- /go_config/reader/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/imdario/mergo" 8 | "github.com/whatvn/denny/go_config/encoder" 9 | "github.com/whatvn/denny/go_config/encoder/json" 10 | "github.com/whatvn/denny/go_config/reader" 11 | "github.com/whatvn/denny/go_config/source" 12 | ) 13 | 14 | type jsonReader struct { 15 | opts reader.Options 16 | json encoder.Encoder 17 | } 18 | 19 | func (j *jsonReader) Merge(changes ...*source.ChangeSet) (*source.ChangeSet, error) { 20 | var merged map[string]interface{} 21 | 22 | for _, m := range changes { 23 | if m == nil { 24 | continue 25 | } 26 | 27 | if len(m.Data) == 0 { 28 | continue 29 | } 30 | 31 | codec, ok := j.opts.Encoding[m.Format] 32 | if !ok { 33 | // fallback 34 | codec = j.json 35 | } 36 | 37 | var data map[string]interface{} 38 | if err := codec.Decode(m.Data, &data); err != nil { 39 | return nil, err 40 | } 41 | if err := mergo.Map(&merged, data, mergo.WithOverride); err != nil { 42 | return nil, err 43 | } 44 | } 45 | 46 | b, err := j.json.Encode(merged) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | cs := &source.ChangeSet{ 52 | Timestamp: time.Now(), 53 | Data: b, 54 | Source: "json", 55 | Format: j.json.String(), 56 | } 57 | cs.Checksum = cs.Sum() 58 | 59 | return cs, nil 60 | } 61 | 62 | func (j *jsonReader) Values(ch *source.ChangeSet) (reader.Values, error) { 63 | if ch == nil { 64 | return nil, errors.New("changeset is nil") 65 | } 66 | if ch.Format != "json" { 67 | return nil, errors.New("unsupported format") 68 | } 69 | return newValues(ch) 70 | } 71 | 72 | func (j *jsonReader) String() string { 73 | return "json" 74 | } 75 | 76 | // NewReader creates a json reader 77 | func NewReader(opts ...reader.Option) reader.Reader { 78 | options := reader.NewOptions(opts...) 79 | return &jsonReader{ 80 | json: json.NewEncoder(), 81 | opts: options, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /go_config/reader/preprocessor_test.go: -------------------------------------------------------------------------------- 1 | package reader 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestReplaceEnvVars(t *testing.T) { 10 | os.Setenv("myBar", "cat") 11 | os.Setenv("MYBAR", "cat") 12 | os.Setenv("my_Bar", "cat") 13 | os.Setenv("myBar_", "cat") 14 | 15 | testData := []struct { 16 | expected string 17 | data []byte 18 | }{ 19 | // Right use cases 20 | { 21 | `{"foo": "bar", "baz": {"bar": "cat"}}`, 22 | []byte(`{"foo": "bar", "baz": {"bar": "${myBar}"}}`), 23 | }, 24 | { 25 | `{"foo": "bar", "baz": {"bar": "cat"}}`, 26 | []byte(`{"foo": "bar", "baz": {"bar": "${MYBAR}"}}`), 27 | }, 28 | { 29 | `{"foo": "bar", "baz": {"bar": "cat"}}`, 30 | []byte(`{"foo": "bar", "baz": {"bar": "${my_Bar}"}}`), 31 | }, 32 | { 33 | `{"foo": "bar", "baz": {"bar": "cat"}}`, 34 | []byte(`{"foo": "bar", "baz": {"bar": "${myBar_}"}}`), 35 | }, 36 | // Wrong use cases 37 | { 38 | `{"foo": "bar", "baz": {"bar": "${myBar-}"}}`, 39 | []byte(`{"foo": "bar", "baz": {"bar": "${myBar-}"}}`), 40 | }, 41 | { 42 | `{"foo": "bar", "baz": {"bar": "${}"}}`, 43 | []byte(`{"foo": "bar", "baz": {"bar": "${}"}}`), 44 | }, 45 | { 46 | `{"foo": "bar", "baz": {"bar": "$sss}"}}`, 47 | []byte(`{"foo": "bar", "baz": {"bar": "$sss}"}}`), 48 | }, 49 | { 50 | `{"foo": "bar", "baz": {"bar": "${sss"}}`, 51 | []byte(`{"foo": "bar", "baz": {"bar": "${sss"}}`), 52 | }, 53 | { 54 | `{"foo": "bar", "baz": {"bar": "{something}"}}`, 55 | []byte(`{"foo": "bar", "baz": {"bar": "{something}"}}`), 56 | }, 57 | // Use cases without replace env vars 58 | { 59 | `{"foo": "bar", "baz": {"bar": "cat"}}`, 60 | []byte(`{"foo": "bar", "baz": {"bar": "cat"}}`), 61 | }, 62 | } 63 | 64 | for _, test := range testData { 65 | res, err := ReplaceEnvVars(test.data) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | if strings.Compare(test.expected, string(res)) != 0 { 70 | t.Fatalf("Expected %s got %s", test.expected, res) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /go_config/source/memory/memory.go: -------------------------------------------------------------------------------- 1 | // Package memory is a memory source 2 | package memory 3 | 4 | import ( 5 | "sync" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | "github.com/whatvn/denny/go_config/source" 10 | ) 11 | 12 | type memory struct { 13 | sync.RWMutex 14 | ChangeSet *source.ChangeSet 15 | Watchers map[string]*watcher 16 | } 17 | 18 | func (s *memory) Read() (*source.ChangeSet, error) { 19 | s.RLock() 20 | cs := &source.ChangeSet{ 21 | Format: s.ChangeSet.Format, 22 | Timestamp: s.ChangeSet.Timestamp, 23 | Data: s.ChangeSet.Data, 24 | Checksum: s.ChangeSet.Checksum, 25 | Source: s.ChangeSet.Source, 26 | } 27 | s.RUnlock() 28 | return cs, nil 29 | } 30 | 31 | func (s *memory) Watch() (source.Watcher, error) { 32 | w := &watcher{ 33 | Id: uuid.New().String(), 34 | Updates: make(chan *source.ChangeSet, 100), 35 | Source: s, 36 | } 37 | 38 | s.Lock() 39 | s.Watchers[w.Id] = w 40 | s.Unlock() 41 | return w, nil 42 | } 43 | 44 | // Update allows manual updates of the config data. 45 | func (s *memory) Update(c *source.ChangeSet) { 46 | // don't process nil 47 | if c == nil { 48 | return 49 | } 50 | 51 | // hash the file 52 | s.Lock() 53 | // update changeset 54 | s.ChangeSet = &source.ChangeSet{ 55 | Data: c.Data, 56 | Format: c.Format, 57 | Source: "memory", 58 | Timestamp: time.Now(), 59 | } 60 | s.ChangeSet.Checksum = s.ChangeSet.Sum() 61 | 62 | // update watchers 63 | for _, w := range s.Watchers { 64 | select { 65 | case w.Updates <- s.ChangeSet: 66 | default: 67 | } 68 | } 69 | s.Unlock() 70 | } 71 | 72 | func (s *memory) String() string { 73 | return "memory" 74 | } 75 | 76 | func NewSource(opts ...source.Option) source.Source { 77 | var options source.Options 78 | for _, o := range opts { 79 | o(&options) 80 | } 81 | 82 | s := &memory{ 83 | Watchers: make(map[string]*watcher), 84 | } 85 | 86 | if options.Context != nil { 87 | c, ok := options.Context.Value(changeSetKey{}).(*source.ChangeSet) 88 | if ok { 89 | s.Update(c) 90 | } 91 | } 92 | 93 | return s 94 | } 95 | -------------------------------------------------------------------------------- /go_config/source/etcd/options.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/whatvn/denny/go_config/source" 8 | ) 9 | 10 | type addressKey struct{} 11 | type pathKey struct{} 12 | type basicAuthKey struct{} 13 | type tlsAuthKey struct{} 14 | type dialTimeoutKey struct{} 15 | 16 | type basicAuthCreds struct { 17 | Username string 18 | Password string 19 | } 20 | 21 | type tlsAuthCreds struct { 22 | CAFile string 23 | CertFile string 24 | KeyFile string 25 | } 26 | 27 | // WithAddress sets the etcd address 28 | func WithAddress(a ...string) source.Option { 29 | return func(o *source.Options) { 30 | if o.Context == nil { 31 | o.Context = context.Background() 32 | } 33 | o.Context = context.WithValue(o.Context, addressKey{}, a) 34 | } 35 | } 36 | 37 | // WithPrefix sets the key prefix to use 38 | func WithPath(p string) source.Option { 39 | return func(o *source.Options) { 40 | if o.Context == nil { 41 | o.Context = context.Background() 42 | } 43 | o.Context = context.WithValue(o.Context, pathKey{}, p) 44 | } 45 | } 46 | 47 | // BasicAuth allows you to specify username/password 48 | func BasicAuth(username, password string) source.Option { 49 | return func(o *source.Options) { 50 | if o.Context == nil { 51 | o.Context = context.Background() 52 | } 53 | o.Context = context.WithValue(o.Context, basicAuthKey{}, &basicAuthCreds{Username: username, Password: password}) 54 | } 55 | } 56 | 57 | // TLSAuth allows you to specify cafile, certfile, keyfile 58 | func TLSAuth(caFile, certFile, keyFile string) source.Option { 59 | return func(o *source.Options) { 60 | if o.Context == nil { 61 | o.Context = context.Background() 62 | } 63 | o.Context = context.WithValue(o.Context, tlsAuthKey{}, &tlsAuthCreds{CAFile: caFile, CertFile: certFile, KeyFile: keyFile}) 64 | } 65 | } 66 | 67 | // WithDialTimeout set the time out for dialing to etcd 68 | func WithDialTimeout(timeout time.Duration) source.Option { 69 | return func(o *source.Options) { 70 | if o.Context == nil { 71 | o.Context = context.Background() 72 | } 73 | o.Context = context.WithValue(o.Context, dialTimeoutKey{}, timeout) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /grpc.go: -------------------------------------------------------------------------------- 1 | package denny 2 | 3 | import ( 4 | "context" 5 | 6 | grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing" 7 | grpc_middleware "github.com/whatvn/denny/middleware/grpc" 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | // ChainUnaryServerInterceptors chains multiple interceptors together. 12 | // 13 | // The first one becomes the outermost, and the last one becomes the 14 | // innermost, i.e. `ChainUnaryServerInterceptors(a, b, c)(h) === a(b(c(h)))`. 15 | // 16 | // nil-valued interceptors are silently skipped. 17 | func chainUnaryServerInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor { 18 | switch { 19 | case len(interceptors) == 0: 20 | // Noop interceptor. 21 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 22 | return handler(ctx, req) 23 | } 24 | case interceptors[0] == nil: 25 | // Skip nils. 26 | return chainUnaryServerInterceptors(interceptors[1:]...) 27 | case len(interceptors) == 1: 28 | // No need to actually chain anything. 29 | return interceptors[0] 30 | default: 31 | return combinator(interceptors[0], chainUnaryServerInterceptors(interceptors[1:]...)) 32 | } 33 | } 34 | 35 | // combinator is an interceptor that chains just two interceptors together. 36 | func combinator(first, second grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor { 37 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 38 | return first(ctx, req, info, func(ctx context.Context, req interface{}) (interface{}, error) { 39 | return second(ctx, req, info, handler) 40 | }) 41 | } 42 | } 43 | 44 | func NewGrpcServer(interceptors ...grpc.UnaryServerInterceptor) *grpc.Server { 45 | var ( 46 | builtinInterceptors = []grpc.UnaryServerInterceptor{ 47 | grpc_middleware.LoggerInterceptor, 48 | grpc_opentracing.UnaryServerInterceptor(), 49 | } 50 | ) 51 | serverInterceptors := chainUnaryServerInterceptors(append(builtinInterceptors, interceptors...)...) 52 | return grpc.NewServer(grpc.UnaryInterceptor(serverInterceptors)) 53 | } 54 | -------------------------------------------------------------------------------- /naming/etcd/naming.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "github.com/whatvn/denny/naming" 6 | "go.etcd.io/etcd/clientv3" 7 | "time" 8 | ) 9 | 10 | // Register starts etcd client and check for registered key, if it's not available 11 | // in etcd storage, it will write service host:port to etcd and start watching to keep writing 12 | // if its data is not available again 13 | func (r *etcd) Register(addr string, ttl int) error { 14 | 15 | var ( 16 | ticker = time.NewTicker(time.Second * time.Duration(ttl)) 17 | err error 18 | svcPath = "/" + naming.Prefix + "/" + r.serviceName + "/" + addr 19 | ) 20 | 21 | r.Infof("register %s with registy", svcPath) 22 | err = r.register(addr, ttl) 23 | if err != nil { 24 | r.Errorf("error %v", err) 25 | } 26 | 27 | go func() { 28 | for { 29 | select { 30 | case _ = <-ticker.C: 31 | resp, err := r.cli.Get(context.Background(), svcPath) 32 | if err != nil { 33 | r.Errorf("error %v", err) 34 | } else if resp.Count == 0 { 35 | err = r.register(addr, ttl) 36 | if err != nil { 37 | r.Errorf("error %v", err) 38 | } 39 | } 40 | case _ = <-r.shutdown: 41 | // receive message from shutdown channel 42 | // will stop current thread and stop ticker to prevent thread leak 43 | ticker.Stop() 44 | return 45 | } 46 | } 47 | }() 48 | 49 | return nil 50 | } 51 | 52 | func (r *etcd) register(addr string, ttl int) error { 53 | leaseResp, err := r.cli.Grant(context.Background(), int64(ttl)) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | _, err = r.cli.Put(context.Background(), "/"+naming.Prefix+"/"+r.serviceName+"/"+addr, addr, clientv3.WithLease(leaseResp.ID)) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | _, err = r.cli.KeepAlive(context.Background(), leaseResp.ID) 64 | if err != nil { 65 | return err 66 | } 67 | return nil 68 | } 69 | 70 | // UnRegister deletes itself in etcd storage 71 | // also stop watch goroutine and ticker inside it 72 | func (r *etcd) UnRegister(addr string) error { 73 | _, _ = r.cli.Delete(context.Background(), "/"+naming.Prefix+"/"+r.serviceName+"/"+addr) 74 | r.shutdown <- "stop" 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | goconfig "github.com/whatvn/denny/go_config" 6 | "github.com/whatvn/denny/go_config/source" 7 | "github.com/whatvn/denny/go_config/source/env" 8 | "github.com/whatvn/denny/go_config/source/etcd" 9 | "github.com/whatvn/denny/go_config/source/file" 10 | "os" 11 | ) 12 | 13 | var ( 14 | cfg Config 15 | ) 16 | 17 | type Config goconfig.Config 18 | 19 | // New will load config from various config file file in Json format 20 | // if same config param available in environment, environment param will 21 | // take higher priority 22 | func New(sources ...string) error { 23 | cfg = goconfig.NewConfig() 24 | if len(sources) == 0 { 25 | return cfg.Load(env.NewSource()) 26 | } 27 | var cfgSources []source.Source 28 | for _, s := range sources { 29 | if !fileExists(s) { 30 | return fmt.Errorf("[Warning] file %s not exist\n", s) 31 | } else { 32 | cfgSources = append(cfgSources, file.NewSource(file.WithPath(s))) 33 | } 34 | } 35 | if err := cfg.Load(cfgSources...); err != nil { 36 | return err 37 | } 38 | 39 | return cfg.Load(env.NewSource()) 40 | } 41 | 42 | func WithEtcd(opt ...source.Option) { 43 | var ( 44 | s = etcd.NewSource(opt...) 45 | ) 46 | if err := cfg.Load(s); err != nil { 47 | panic(err) 48 | } 49 | } 50 | 51 | func Watch() (goconfig.Watcher, error) { 52 | return cfg.Watch() 53 | } 54 | 55 | func Reload() error { 56 | return cfg.Sync() 57 | } 58 | 59 | func GetString(path ...string) string { 60 | return cfg.Get(path...).String("") 61 | } 62 | 63 | func GetStringMap(path ...string) map[string]string { 64 | return cfg.Get(path...).StringMap(nil) 65 | } 66 | 67 | func GetStringArray(path ...string) []string { 68 | return cfg.Get(path...).StringSlice(nil) 69 | } 70 | 71 | func GetInt(path ...string) int { 72 | return cfg.Get(path...).Int(0) 73 | } 74 | 75 | func Scan(t interface{}, path ...string) error { 76 | return cfg.Get(path...).Scan(t) 77 | } 78 | 79 | func Map() map[string]interface{} { 80 | return cfg.Map() 81 | } 82 | 83 | func fileExists(path string) bool { 84 | fh, err := os.Open(path) 85 | defer func() { 86 | if fh != nil { 87 | _ = fh.Close() 88 | } 89 | }() 90 | if err != nil { 91 | return false 92 | } 93 | return true 94 | } 95 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/whatvn/denny 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/HdrHistogram/hdrhistogram-go v1.0.1 // indirect 8 | github.com/astaxie/beego v1.12.3 9 | github.com/bitly/go-simplejson v0.5.0 10 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect 11 | github.com/coreos/etcd v3.3.13+incompatible 12 | github.com/envoyproxy/protoc-gen-validate v0.1.0 13 | github.com/fsnotify/fsnotify v1.4.9 14 | github.com/ghodss/yaml v1.0.0 15 | github.com/gin-gonic/gin v1.7.0 16 | github.com/go-redis/redis v6.15.9+incompatible 17 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 18 | github.com/golang/protobuf v1.4.3 19 | github.com/google/uuid v1.1.2 20 | github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 21 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 22 | github.com/hashicorp/hcl v1.0.0 23 | github.com/imdario/mergo v0.3.8 24 | github.com/jonboulle/clockwork v0.2.2 // indirect 25 | github.com/kr/pretty v0.2.1 // indirect 26 | github.com/onsi/ginkgo v1.14.2 // indirect 27 | github.com/onsi/gomega v1.10.4 // indirect 28 | github.com/opentracing/opentracing-go v1.1.0 29 | github.com/prometheus/client_golang v1.9.0 // indirect 30 | github.com/sirupsen/logrus v1.6.0 31 | github.com/soheilhy/cmux v0.1.4 32 | github.com/stretchr/testify v1.6.1 33 | github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect 34 | github.com/uber/jaeger-client-go v2.20.1+incompatible 35 | github.com/uber/jaeger-lib v2.4.0+incompatible // indirect 36 | go.etcd.io/etcd v3.3.22+incompatible 37 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 38 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect 39 | google.golang.org/grpc v1.33.1 40 | google.golang.org/protobuf v1.25.0 41 | sigs.k8s.io/yaml v1.2.0 // indirect 42 | ) 43 | 44 | replace ( 45 | github.com/codahale/hdrhistogram => github.com/HdrHistogram/hdrhistogram-go v1.0.0 46 | github.com/coreos/bbolt => go.etcd.io/bbolt v1.3.5 47 | github.com/coreos/etcd => github.com/ozonru/etcd v3.3.20-grpc1.27-origmodule+incompatible 48 | github.com/coreos/go-systemd => github.com/coreos/go-systemd/v22 v22.0.0 49 | google.golang.org/grpc => google.golang.org/grpc v1.27.0 50 | ) 51 | -------------------------------------------------------------------------------- /go_config/source/flag/flag.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "github.com/imdario/mergo" 7 | "github.com/whatvn/denny/go_config/source" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type flagsrc struct { 13 | opts source.Options 14 | } 15 | 16 | func (fs *flagsrc) Read() (*source.ChangeSet, error) { 17 | if !flag.Parsed() { 18 | return nil, errors.New("flags not parsed") 19 | } 20 | 21 | var changes map[string]interface{} 22 | 23 | visitFn := func(f *flag.Flag) { 24 | n := strings.ToLower(f.Name) 25 | keys := strings.FieldsFunc(n, split) 26 | reverse(keys) 27 | 28 | tmp := make(map[string]interface{}) 29 | for i, k := range keys { 30 | if i == 0 { 31 | tmp[k] = f.Value 32 | continue 33 | } 34 | 35 | tmp = map[string]interface{}{k: tmp} 36 | } 37 | 38 | mergo.Map(&changes, tmp) // need to sort error handling 39 | return 40 | } 41 | 42 | unset, ok := fs.opts.Context.Value(includeUnsetKey{}).(bool) 43 | if ok && unset { 44 | flag.VisitAll(visitFn) 45 | } else { 46 | flag.Visit(visitFn) 47 | } 48 | 49 | b, err := fs.opts.Encoder.Encode(changes) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | cs := &source.ChangeSet{ 55 | Format: fs.opts.Encoder.String(), 56 | Data: b, 57 | Timestamp: time.Now(), 58 | Source: fs.String(), 59 | } 60 | cs.Checksum = cs.Sum() 61 | 62 | return cs, nil 63 | } 64 | 65 | func split(r rune) bool { 66 | return r == '-' || r == '_' 67 | } 68 | 69 | func reverse(ss []string) { 70 | for i := len(ss)/2 - 1; i >= 0; i-- { 71 | opp := len(ss) - 1 - i 72 | ss[i], ss[opp] = ss[opp], ss[i] 73 | } 74 | } 75 | 76 | func (fs *flagsrc) Watch() (source.Watcher, error) { 77 | return source.NewNoopWatcher() 78 | } 79 | 80 | func (fs *flagsrc) String() string { 81 | return "flag" 82 | } 83 | 84 | // NewSource returns a config source for integrating parsed flags. 85 | // Hyphens are delimiters for nesting, and all keys are lowercased. 86 | // 87 | // Example: 88 | // dbhost := flag.String("database-host", "localhost", "the db host name") 89 | // 90 | // { 91 | // "database": { 92 | // "host": "localhost" 93 | // } 94 | // } 95 | func NewSource(opts ...source.Option) source.Source { 96 | return &flagsrc{opts: source.NewOptions(opts...)} 97 | } 98 | -------------------------------------------------------------------------------- /go_config/config.go: -------------------------------------------------------------------------------- 1 | // Package config is an interface for dynamic configuration. 2 | package config 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/whatvn/denny/go_config/loader" 8 | "github.com/whatvn/denny/go_config/reader" 9 | "github.com/whatvn/denny/go_config/source" 10 | "github.com/whatvn/denny/go_config/source/file" 11 | ) 12 | 13 | // Config is an interface abstraction for dynamic configuration 14 | type Config interface { 15 | // provide the reader.Values interface 16 | reader.Values 17 | // Stop the config loader/watcher 18 | Close() error 19 | // Load config sources 20 | Load(source ...source.Source) error 21 | // Force a source changeset sync 22 | Sync() error 23 | // Watch a value for changes 24 | Watch(path ...string) (Watcher, error) 25 | } 26 | 27 | // Watcher is the config watcher 28 | type Watcher interface { 29 | Next() (reader.Value, error) 30 | Stop() error 31 | } 32 | 33 | type Options struct { 34 | Loader loader.Loader 35 | Reader reader.Reader 36 | Source []source.Source 37 | 38 | // for alternative data 39 | Context context.Context 40 | } 41 | 42 | type Option func(o *Options) 43 | 44 | var ( 45 | // Default Config Manager 46 | DefaultConfig = NewConfig() 47 | ) 48 | 49 | // NewConfig returns new config 50 | func NewConfig(opts ...Option) Config { 51 | return newConfig(opts...) 52 | } 53 | 54 | // Return config as raw json 55 | func Bytes() []byte { 56 | return DefaultConfig.Bytes() 57 | } 58 | 59 | // Return config as a map 60 | func Map() map[string]interface{} { 61 | return DefaultConfig.Map() 62 | } 63 | 64 | // Scan values to a go type 65 | func Scan(v interface{}) error { 66 | return DefaultConfig.Scan(v) 67 | } 68 | 69 | // Force a source changeset sync 70 | func Sync() error { 71 | return DefaultConfig.Sync() 72 | } 73 | 74 | // Get a value from the config 75 | func Get(path ...string) reader.Value { 76 | return DefaultConfig.Get(path...) 77 | } 78 | 79 | // Load config sources 80 | func Load(source ...source.Source) error { 81 | return DefaultConfig.Load(source...) 82 | } 83 | 84 | // Watch a value for changes 85 | func Watch(path ...string) (Watcher, error) { 86 | return DefaultConfig.Watch(path...) 87 | } 88 | 89 | // LoadFile is short hand for creating a file source and loading it 90 | func LoadFile(path string) error { 91 | return Load(file.NewSource( 92 | file.WithPath(path), 93 | )) 94 | } 95 | -------------------------------------------------------------------------------- /cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | redisCli "github.com/go-redis/redis" 5 | "time" 6 | ) 7 | 8 | type redis struct { 9 | cli *redisCli.Client 10 | } 11 | 12 | // Get return value if key exist or nil if it does not 13 | func (c *redis) Get(key string) interface{} { 14 | cmd := c.cli.Get(key) 15 | s, err := cmd.Result() 16 | if err != nil { 17 | return nil 18 | } 19 | return s 20 | } 21 | 22 | // GetOrElse return value if it exists, else warmup using warmup function 23 | func (c *redis) GetOrElse(key string, wuf func(key string) interface{}, expire ...int64) interface{} { 24 | v := c.Get(key) 25 | if v != nil { 26 | return v 27 | } 28 | if v := wuf(key); v != nil { 29 | var expired int64 = 0 30 | if len(expire) > 0 { 31 | expired = expire[0] 32 | } 33 | c.Set(key, v, expired) 34 | return v 35 | } 36 | return nil 37 | } 38 | 39 | // Set store key in sync map 40 | func (c *redis) Set(key string, val interface{}, expire int64) { 41 | c.cli.Set(key, val, time.Duration(expire)*time.Second) 42 | } 43 | 44 | // Get multi will load all values with given keys 45 | // caller has to check request return value against nil befors using 46 | // as this call will not check key existence 47 | func (c *redis) GetMulti(keys []string) []interface{} { 48 | cmd := c.cli.MGet(keys...) 49 | result, err := cmd.Result() 50 | if err != nil { 51 | return nil 52 | } 53 | return result 54 | } 55 | 56 | // Delete delete key in map if it exists 57 | func (c *redis) Delete(key string) { 58 | c.cli.Del(key) 59 | } 60 | 61 | // Incr incr key in map if it exists 62 | func (c *redis) Incr(key string) error { 63 | cmd := c.cli.Incr(key) 64 | if _, err := cmd.Result(); err != nil { 65 | return err 66 | } 67 | return nil 68 | } 69 | 70 | // Incr incr key in map if it exists 71 | func (c *redis) Decr(key string) error { 72 | cmd := c.cli.Decr(key) 73 | if _, err := cmd.Result(); err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | 79 | func (c *redis) IsExist(key string) bool { 80 | cmd := c.cli.Exists(key) 81 | val, err := cmd.Result() 82 | if err != nil { 83 | return false 84 | } 85 | return val > 0 86 | } 87 | 88 | func (c *redis) ClearAll() { 89 | c.cli.FlushAll() 90 | } 91 | 92 | func (c *redis) runGc(config Config) { 93 | } 94 | 95 | func (c *redis) expires() []string { 96 | return nil 97 | } 98 | 99 | func NewRedis(address, password string) Cache { 100 | c := &redis{ 101 | cli: redisCli.NewClient(&redisCli.Options{ 102 | Addr: address, 103 | Password: password, 104 | }), 105 | } 106 | return c 107 | } 108 | -------------------------------------------------------------------------------- /go_config/default_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/whatvn/denny/go_config/source/env" 12 | "github.com/whatvn/denny/go_config/source/file" 13 | ) 14 | 15 | var ( 16 | sep = string(os.PathSeparator) 17 | ) 18 | 19 | func createFileForIssue18(t *testing.T, content string) *os.File { 20 | data := []byte(content) 21 | path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano())) 22 | fh, err := os.Create(path) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | _, err = fh.Write(data) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | 31 | return fh 32 | } 33 | 34 | func createFileForTest(t *testing.T) *os.File { 35 | data := []byte(`{"foo": "bar"}`) 36 | path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano())) 37 | fh, err := os.Create(path) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | _, err = fh.Write(data) 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | 46 | return fh 47 | } 48 | 49 | func TestConfigLoadWithGoodFile(t *testing.T) { 50 | fh := createFileForTest(t) 51 | path := fh.Name() 52 | defer func() { 53 | fh.Close() 54 | os.Remove(path) 55 | }() 56 | 57 | // Create new config 58 | conf := NewConfig() 59 | // Load file source 60 | if err := conf.Load(file.NewSource( 61 | file.WithPath(path), 62 | )); err != nil { 63 | t.Fatalf("Expected no error but got %v", err) 64 | } 65 | } 66 | 67 | func TestConfigLoadWithInvalidFile(t *testing.T) { 68 | fh := createFileForTest(t) 69 | path := fh.Name() 70 | defer func() { 71 | fh.Close() 72 | os.Remove(path) 73 | }() 74 | 75 | // Create new config 76 | conf := NewConfig() 77 | // Load file source 78 | err := conf.Load(file.NewSource( 79 | file.WithPath(path), 80 | file.WithPath("/i/do/not/exists.json"), 81 | )) 82 | 83 | if err == nil { 84 | t.Fatal("Expected error but none !") 85 | } 86 | if !strings.Contains(fmt.Sprintf("%v", err), "/i/do/not/exists.json") { 87 | t.Fatalf("Expected error to contain the unexisting file but got %v", err) 88 | } 89 | } 90 | 91 | func TestConfigMerge(t *testing.T) { 92 | fh := createFileForIssue18(t, `{ 93 | "amqp": { 94 | "host": "rabbit.platform", 95 | "port": 80 96 | }, 97 | "handler": { 98 | "exchange": "springCloudBus" 99 | } 100 | }`) 101 | path := fh.Name() 102 | defer func() { 103 | fh.Close() 104 | os.Remove(path) 105 | }() 106 | os.Setenv("AMQP_HOST", "rabbit.testing.com") 107 | 108 | conf := NewConfig() 109 | conf.Load( 110 | file.NewSource( 111 | file.WithPath(path), 112 | ), 113 | env.NewSource(), 114 | ) 115 | 116 | actualHost := conf.Get("amqp", "host").String("backup") 117 | if actualHost != "rabbit.testing.com" { 118 | t.Fatalf("Expected %v but got %v", 119 | "rabbit.testing.com", 120 | actualHost) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /naming/redis/resolver.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | "github.com/whatvn/denny/naming" 6 | "google.golang.org/grpc/resolver" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // NewResolver is alias to New(), and also register resolver automatically 12 | // so client does not have to call register resolver everytime 13 | func NewResolver(redisAddr, redisPwd, serviceName string) naming.Registry { 14 | registry := New(redisAddr, redisPwd, serviceName) 15 | resolver.Register(registry) 16 | return registry 17 | } 18 | 19 | func (r *redis) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { 20 | if r.cli == nil { 21 | return nil, errors.New("etcd client was not initialised") 22 | } 23 | r.cc = cc 24 | r.WithFields(map[string]interface{}{ 25 | "scheme": target.Scheme, 26 | "endpoint": target.Endpoint, 27 | }) 28 | keyPrefix := "/" + target.Scheme + "/" + target.Endpoint + "/" 29 | go r.watch(keyPrefix) 30 | return r, nil 31 | } 32 | 33 | func (r *redis) watch(keyPrefix string) { 34 | var ( 35 | addrList []resolver.Address 36 | ticker = time.NewTicker(time.Duration(5) * time.Second) 37 | ) 38 | 39 | list, err := r.addressList(keyPrefix, addrList) 40 | if err != nil { 41 | r.Errorf("cannot get address list: %v", err) 42 | } else { 43 | addrList = list[:] 44 | } 45 | 46 | r.cc.UpdateState(resolver.State{ 47 | Addresses: addrList, 48 | }) 49 | 50 | for { 51 | select { 52 | case _ = <-ticker.C: 53 | updatedList, err := r.addressList(keyPrefix, addrList) 54 | if err != nil { 55 | r.Errorf("cannot get address list: %v", err) 56 | } else { 57 | needUpdate := false 58 | // append to state list if it's not exist 59 | for _, addr := range updatedList { 60 | if !naming.Exist(addrList, addr.Addr) { 61 | needUpdate = true 62 | addrList = append(addrList, addr) 63 | } 64 | } 65 | 66 | // remove dead peer 67 | for _, addr := range addrList { 68 | if !naming.Exist(updatedList, addr.Addr) { 69 | needUpdate = true 70 | if s, ok := naming.Remove(addrList, addr.Addr); ok { 71 | addrList = s 72 | } 73 | } 74 | } 75 | 76 | if needUpdate { 77 | r.cc.UpdateState(resolver.State{Addresses: addrList}) 78 | } 79 | } 80 | 81 | } 82 | 83 | } 84 | } 85 | 86 | func (r *redis) addressList(keyPrefix string, addrList []resolver.Address) ([]resolver.Address, error) { 87 | resp, err := r.cli.Keys(keyPrefix + "*").Result() 88 | if err != nil { 89 | return nil, err 90 | } 91 | for _, key := range resp { 92 | addrList = append(addrList, resolver.Address{Addr: strings.TrimPrefix(key, keyPrefix)}) 93 | } 94 | return addrList, nil 95 | } 96 | 97 | func (r redis) Scheme() string { 98 | return naming.Prefix 99 | } 100 | 101 | func (r redis) SvcName() string { 102 | return r.Scheme() + ":///" + r.serviceName 103 | } 104 | 105 | func (r *redis) ResolveNow(resolver.ResolveNowOptions) { 106 | // TODO 107 | } 108 | 109 | func (r *redis) Close() { 110 | _ = r.cli.Close() 111 | } 112 | -------------------------------------------------------------------------------- /go_config/source/env/env_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/whatvn/denny/go_config/source" 10 | ) 11 | 12 | func TestEnv_Read(t *testing.T) { 13 | expected := map[string]map[string]string{ 14 | "database": { 15 | "host": "localhost", 16 | "password": "password", 17 | "datasource": "user:password@tcp(localhost:port)/db?charset=utf8mb4&parseTime=True&loc=Local", 18 | }, 19 | } 20 | 21 | os.Setenv("DATABASE_HOST", "localhost") 22 | os.Setenv("DATABASE_PASSWORD", "password") 23 | os.Setenv("DATABASE_DATASOURCE", "user:password@tcp(localhost:port)/db?charset=utf8mb4&parseTime=True&loc=Local") 24 | 25 | source := NewSource() 26 | c, err := source.Read() 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | 31 | var actual map[string]interface{} 32 | if err := json.Unmarshal(c.Data, &actual); err != nil { 33 | t.Error(err) 34 | } 35 | 36 | actualDB := actual["database"].(map[string]interface{}) 37 | 38 | for k, v := range expected["database"] { 39 | a := actualDB[k] 40 | 41 | if a != v { 42 | t.Errorf("expected %v got %v", v, a) 43 | } 44 | } 45 | } 46 | 47 | func TestEnvvar_Prefixes(t *testing.T) { 48 | os.Setenv("APP_DATABASE_HOST", "localhost") 49 | os.Setenv("APP_DATABASE_PASSWORD", "password") 50 | os.Setenv("VAULT_ADDR", "vault:1337") 51 | os.Setenv("MICRO_REGISTRY", "mdns") 52 | 53 | var prefixtests = []struct { 54 | prefixOpts []source.Option 55 | expectedKeys []string 56 | }{ 57 | {[]source.Option{WithPrefix("APP", "MICRO")}, []string{"app", "micro"}}, 58 | {[]source.Option{WithPrefix("MICRO"), WithStrippedPrefix("APP")}, []string{"database", "micro"}}, 59 | {[]source.Option{WithPrefix("MICRO"), WithStrippedPrefix("APP")}, []string{"database", "micro"}}, 60 | } 61 | 62 | for _, pt := range prefixtests { 63 | source := NewSource(pt.prefixOpts...) 64 | 65 | c, err := source.Read() 66 | if err != nil { 67 | t.Error(err) 68 | } 69 | 70 | var actual map[string]interface{} 71 | if err := json.Unmarshal(c.Data, &actual); err != nil { 72 | t.Error(err) 73 | } 74 | 75 | // assert other prefixes ignored 76 | if l := len(actual); l != len(pt.expectedKeys) { 77 | t.Errorf("expected %v top keys, got %v", len(pt.expectedKeys), l) 78 | } 79 | 80 | for _, k := range pt.expectedKeys { 81 | if !containsKey(actual, k) { 82 | t.Errorf("expected key %v, not found", k) 83 | } 84 | } 85 | } 86 | } 87 | 88 | func TestEnvvar_WatchNextNoOpsUntilStop(t *testing.T) { 89 | src := NewSource(WithStrippedPrefix("GOMICRO_")) 90 | w, err := src.Watch() 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | 95 | go func() { 96 | time.Sleep(50 * time.Millisecond) 97 | w.Stop() 98 | }() 99 | 100 | if _, err := w.Next(); err != source.ErrWatcherStopped { 101 | t.Errorf("expected watcher stopped error, got %v", err) 102 | } 103 | } 104 | 105 | func containsKey(m map[string]interface{}, s string) bool { 106 | for k := range m { 107 | if k == s { 108 | return true 109 | } 110 | } 111 | return false 112 | } 113 | -------------------------------------------------------------------------------- /example/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/opentracing/opentracing-go" 5 | "github.com/uber/jaeger-client-go" 6 | zk "github.com/uber/jaeger-client-go/transport/zipkin" 7 | "github.com/uber/jaeger-client-go/zipkin" 8 | "github.com/whatvn/denny/middleware/http" 9 | "github.com/whatvn/denny/middleware/http/ot" 10 | "io/ioutil" 11 | http_lib "net/http" 12 | 13 | "github.com/whatvn/denny" 14 | ) 15 | 16 | type xController struct { 17 | denny.Controller 18 | } 19 | 20 | func (x xController) Handle(ctx *denny.Context) { 21 | var logger = denny.GetLogger(ctx) 22 | 23 | logger.AddLog("log something to test log init") 24 | logger.WithField("x", "y") 25 | logger.AddLog("receive request") 26 | var str = "hello" 27 | logger.AddLog("do more thing") 28 | str += " world" 29 | logger.AddLog("finished") 30 | ctx.Writer.Write([]byte(str)) 31 | } 32 | 33 | type yController struct { 34 | denny.Controller 35 | } 36 | 37 | func (y yController) Handle(ctx *denny.Context) { 38 | y.AddLog("receive request") 39 | var str = "hello" 40 | var logger = denny.GetLogger(ctx) 41 | logger.AddLog("do more thing") 42 | str += " denny" 43 | y.Infof("finished") 44 | cli := http_lib.Client{} 45 | req, _ := http_lib.NewRequest("GET", "http://127.0.0.1:8081/", nil) 46 | var span, ok = ot.GetSpan(ctx) 47 | if ok { 48 | opentracing.GlobalTracer().Inject( 49 | span.Context(), 50 | opentracing.HTTPHeaders, 51 | opentracing.HTTPHeadersCarrier(req.Header)) 52 | defer func() { 53 | span.Finish() 54 | }() 55 | } 56 | 57 | logger.AddLog("headers", req.Header) 58 | response, _ := cli.Do(req.WithContext(ctx)) 59 | bytes, _ := ioutil.ReadAll(response.Body) 60 | ctx.Writer.Write([]byte(str + " remote " + string(bytes))) 61 | } 62 | 63 | func newReporter() jaeger.Transport { 64 | transport, err := zk.NewHTTPTransport( 65 | "http://10.109.3.93:9411/api/v1/spans", 66 | zk.HTTPBatchSize(10), 67 | zk.HTTPLogger(jaeger.StdLogger), 68 | ) 69 | if err != nil { 70 | panic(err) 71 | } 72 | return transport 73 | } 74 | 75 | func main() { 76 | server := denny.NewServer() 77 | propagator := zipkin.NewZipkinB3HTTPHeaderPropagator() 78 | trace, closer := jaeger.NewTracer( 79 | "api_gateway", 80 | jaeger.NewConstSampler(true), 81 | jaeger.NewRemoteReporter(newReporter()), 82 | jaeger.TracerOptions.Injector(opentracing.HTTPHeaders, propagator), 83 | jaeger.TracerOptions.Extractor(opentracing.HTTPHeaders, propagator), 84 | jaeger.TracerOptions.ZipkinSharedRPCSpan(true), 85 | ) 86 | defer closer.Close() 87 | 88 | authorized := server.NewGroup("/") 89 | // per group middleware! in this case we use the custom created 90 | // AuthRequired() middleware just in the "authorized" group. 91 | authorized.Use(http.Logger()) 92 | { 93 | authorized.Controller("/login", denny.HttpGet, &xController{}) 94 | authorized.Controller("/logout", denny.HttpGet, &xController{}) 95 | authorized.Controller("/profile", denny.HttpGet, &xController{}) 96 | // nested group 97 | } 98 | opentracing.SetGlobalTracer(trace) 99 | 100 | server.Use(http.Logger()).Use(ot.RequestTracer()) 101 | server.Controller("/", denny.HttpGet, &xController{}) 102 | server.Controller("/denny", denny.HttpGet, &yController{}) 103 | server.GraceFulStart() 104 | } 105 | -------------------------------------------------------------------------------- /naming/etcd/resolver.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/coreos/etcd/mvcc/mvccpb" 7 | "github.com/whatvn/denny/naming" 8 | "go.etcd.io/etcd/clientv3" 9 | "google.golang.org/grpc/resolver" 10 | "strings" 11 | ) 12 | 13 | // NewResolver is alias to New(), and also register resolver automatically 14 | // so client does not have to call register resolver everytime 15 | func NewResolver(etcdAddrs, serviceName string) naming.Registry { 16 | registry := New(etcdAddrs, serviceName) 17 | resolver.Register(registry) 18 | return registry 19 | } 20 | 21 | // NewResolverWithClientConfig is alias to NewWithClientConfig(), and also register resolver automatically 22 | // so client does not have to call register resolver everytime 23 | func NewResolverWithClientConfig(serviceName string, etcdClientCfg clientv3.Config) naming.Registry { 24 | registry := NewWithClientConfig(serviceName, etcdClientCfg) 25 | resolver.Register(registry) 26 | return registry 27 | } 28 | 29 | // Build implements grpc Builder.Build method so grpc client know how to construct resolver Builder 30 | func (r *etcd) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { 31 | if r.cli == nil { 32 | return nil, errors.New("etcd client was not initialised") 33 | } 34 | r.cc = cc 35 | r.WithFields(map[string]interface{}{ 36 | "scheme": target.Scheme, 37 | "endpoint": target.Endpoint, 38 | }) 39 | keyPrefix := "/" + target.Scheme + "/" + target.Endpoint + "/" 40 | go r.watch(keyPrefix) 41 | return r, nil 42 | } 43 | 44 | // Scheme implements Builder.Scheme method to get prefix hint for grpc resolver 45 | func (r etcd) Scheme() string { 46 | return naming.Prefix 47 | } 48 | 49 | // SvcName is shortcut for client's user, it return full service url 50 | // so clients does not have to construct service url themself 51 | func (r etcd) SvcName() string { 52 | return r.Scheme() + ":///" + r.serviceName 53 | } 54 | 55 | // ResolveNow force grpc clients to resolve service address immediately 56 | // it's TODO implementation 57 | func (r etcd) ResolveNow(rn resolver.ResolveNowOptions) { 58 | // will force to update address list immediately 59 | } 60 | 61 | // Close closes the resolver. 62 | func (r etcd) Close() { 63 | _ = r.cli.Close() 64 | } 65 | 66 | func (r *etcd) watch(keyPrefix string) { 67 | var addrList []resolver.Address 68 | 69 | resp, err := r.cli.Get(context.Background(), keyPrefix, clientv3.WithPrefix()) 70 | if err != nil { 71 | r.Errorf("error %v", err) 72 | } else { 73 | for i := range resp.Kvs { 74 | addrList = append(addrList, resolver.Address{Addr: strings.TrimPrefix(string(resp.Kvs[i].Key), keyPrefix)}) 75 | } 76 | } 77 | 78 | r.cc.UpdateState(resolver.State{ 79 | Addresses: addrList, 80 | }) 81 | 82 | rch := r.cli.Watch(context.Background(), keyPrefix, clientv3.WithPrefix()) 83 | for n := range rch { 84 | for _, ev := range n.Events { 85 | addr := strings.TrimPrefix(string(ev.Kv.Key), keyPrefix) 86 | switch ev.Type { 87 | case mvccpb.PUT: 88 | if !naming.Exist(addrList, addr) { 89 | addrList = append(addrList, resolver.Address{Addr: addr}) 90 | r.cc.UpdateState(resolver.State{Addresses: addrList}) 91 | } 92 | case mvccpb.DELETE: 93 | if s, ok := naming.Remove(addrList, addr); ok { 94 | addrList = s 95 | r.cc.UpdateState(resolver.State{Addresses: addrList}) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /go_config/source/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/imdario/mergo" 10 | "github.com/whatvn/denny/go_config/source" 11 | ) 12 | 13 | var ( 14 | DefaultPrefixes = []string{} 15 | ) 16 | 17 | type env struct { 18 | prefixes []string 19 | strippedPrefixes []string 20 | opts source.Options 21 | } 22 | 23 | func (e *env) Read() (*source.ChangeSet, error) { 24 | var changes map[string]interface{} 25 | 26 | for _, env := range os.Environ() { 27 | 28 | if len(e.prefixes) > 0 || len(e.strippedPrefixes) > 0 { 29 | notFound := true 30 | 31 | if _, ok := matchPrefix(e.prefixes, env); ok { 32 | notFound = false 33 | } 34 | 35 | if match, ok := matchPrefix(e.strippedPrefixes, env); ok { 36 | env = strings.TrimPrefix(env, match) 37 | notFound = false 38 | } 39 | 40 | if notFound { 41 | continue 42 | } 43 | } 44 | 45 | pair := strings.SplitN(env, "=", 2) 46 | value := pair[1] 47 | keys := strings.Split(strings.ToLower(pair[0]), "_") 48 | reverse(keys) 49 | 50 | tmp := make(map[string]interface{}) 51 | for i, k := range keys { 52 | if i == 0 { 53 | if intValue, err := strconv.Atoi(value); err == nil { 54 | tmp[k] = intValue 55 | } else if boolValue, err := strconv.ParseBool(value); err == nil { 56 | tmp[k] = boolValue 57 | } else { 58 | tmp[k] = value 59 | } 60 | continue 61 | } 62 | 63 | tmp = map[string]interface{}{k: tmp} 64 | } 65 | 66 | if err := mergo.Map(&changes, tmp); err != nil { 67 | return nil, err 68 | } 69 | } 70 | 71 | b, err := e.opts.Encoder.Encode(changes) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | cs := &source.ChangeSet{ 77 | Format: e.opts.Encoder.String(), 78 | Data: b, 79 | Timestamp: time.Now(), 80 | Source: e.String(), 81 | } 82 | cs.Checksum = cs.Sum() 83 | 84 | return cs, nil 85 | } 86 | 87 | func matchPrefix(pre []string, s string) (string, bool) { 88 | for _, p := range pre { 89 | if strings.HasPrefix(s, p) { 90 | return p, true 91 | } 92 | } 93 | 94 | return "", false 95 | } 96 | 97 | func reverse(ss []string) { 98 | for i := len(ss)/2 - 1; i >= 0; i-- { 99 | opp := len(ss) - 1 - i 100 | ss[i], ss[opp] = ss[opp], ss[i] 101 | } 102 | } 103 | 104 | func (e *env) Watch() (source.Watcher, error) { 105 | return newWatcher() 106 | } 107 | 108 | func (e *env) String() string { 109 | return "env" 110 | } 111 | 112 | // NewSource returns a config source for parsing ENV variables. 113 | // Underscores are delimiters for nesting, and all keys are lowercased. 114 | // 115 | // Example: 116 | // "DATABASE_SERVER_HOST=localhost" will convert to 117 | // 118 | // { 119 | // "database": { 120 | // "server": { 121 | // "host": "localhost" 122 | // } 123 | // } 124 | // } 125 | func NewSource(opts ...source.Option) source.Source { 126 | options := source.NewOptions(opts...) 127 | 128 | var sp []string 129 | var pre []string 130 | if p, ok := options.Context.Value(strippedPrefixKey{}).([]string); ok { 131 | sp = p 132 | } 133 | 134 | if p, ok := options.Context.Value(prefixKey{}).([]string); ok { 135 | pre = p 136 | } 137 | 138 | if len(sp) > 0 || len(pre) > 0 { 139 | pre = append(pre, DefaultPrefixes...) 140 | } 141 | return &env{prefixes: pre, strippedPrefixes: sp, opts: options} 142 | } 143 | -------------------------------------------------------------------------------- /go_config/source/etcd/etcd.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "time" 9 | 10 | "github.com/whatvn/denny/go_config/source" 11 | cetcd "go.etcd.io/etcd/clientv3" 12 | "go.etcd.io/etcd/pkg/transport" 13 | ) 14 | 15 | // Currently a single etcd reader 16 | type etcd struct { 17 | path string 18 | opts source.Options 19 | client *cetcd.Client 20 | cerr error 21 | } 22 | 23 | func (c *etcd) Read() (*source.ChangeSet, error) { 24 | if c.cerr != nil { 25 | return nil, c.cerr 26 | } 27 | 28 | rsp, err := c.client.Get(context.Background(), c.path) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | if rsp == nil || len(rsp.Kvs) == 0 { 34 | return nil, fmt.Errorf("source not found: %s", c.path) 35 | } 36 | 37 | b := rsp.Kvs[0].Value 38 | 39 | cs := &source.ChangeSet{ 40 | Timestamp: time.Now(), 41 | Source: c.String(), 42 | Data: b, 43 | Format: c.opts.Encoder.String(), 44 | } 45 | cs.Checksum = cs.Sum() 46 | 47 | return cs, nil 48 | } 49 | 50 | func (c *etcd) String() string { 51 | return "etcd" 52 | } 53 | 54 | func (c *etcd) Watch() (source.Watcher, error) { 55 | if c.cerr != nil { 56 | return nil, c.cerr 57 | } 58 | cs, err := c.Read() 59 | if err != nil { 60 | return nil, err 61 | } 62 | return newWatcher(c.path, c.client.Watcher, cs, c.opts) 63 | } 64 | 65 | func NewSource(opts ...source.Option) source.Source { 66 | options := source.NewOptions(opts...) 67 | 68 | var endpoints []string 69 | 70 | // check if there are any addrs 71 | addrs, ok := options.Context.Value(addressKey{}).([]string) 72 | if ok { 73 | for _, a := range addrs { 74 | addr, port, err := net.SplitHostPort(a) 75 | if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { 76 | port = "2379" 77 | addr = a 78 | endpoints = append(endpoints, fmt.Sprintf("%s:%s", addr, port)) 79 | } else if err == nil { 80 | endpoints = append(endpoints, fmt.Sprintf("%s:%s", addr, port)) 81 | } 82 | } 83 | } 84 | 85 | if len(endpoints) == 0 { 86 | endpoints = []string{"localhost:2379"} 87 | } 88 | 89 | // check dial timeout option 90 | dialTimeout, ok := options.Context.Value(dialTimeoutKey{}).(time.Duration) 91 | if !ok { 92 | dialTimeout = 3 * time.Second // default dial timeout 93 | } 94 | 95 | config := cetcd.Config{ 96 | Endpoints: endpoints, 97 | DialTimeout: dialTimeout, 98 | } 99 | 100 | u, ok := options.Context.Value(basicAuthKey{}).(*basicAuthCreds) 101 | if ok { 102 | config.Username = u.Username 103 | config.Password = u.Password 104 | } 105 | 106 | tls, ok := options.Context.Value(tlsAuthKey{}).(*tlsAuthCreds) 107 | if ok { 108 | var ( 109 | cfgTLS *transport.TLSInfo 110 | err error 111 | ) 112 | cfgTLS = &transport.TLSInfo{ 113 | CertFile: tls.CertFile, 114 | KeyFile: tls.KeyFile, 115 | CAFile: tls.CAFile, 116 | } 117 | config.TLS, err = cfgTLS.ClientConfig() 118 | if err != nil { 119 | panic(err) 120 | } 121 | } 122 | 123 | // use default config 124 | client, err := cetcd.New(config) 125 | 126 | path, ok := options.Context.Value(pathKey{}).(string) 127 | 128 | if !ok { 129 | panic("cannot setup etcd source with empty path") 130 | } 131 | 132 | if strings.HasSuffix(path, "/") { 133 | panic("etcd path cannot be directory") 134 | } 135 | 136 | return &etcd{ 137 | path: path, 138 | opts: options, 139 | client: client, 140 | cerr: err, 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func createFileForTest(t *testing.T) *os.File { 13 | data := []byte(`{"foo": "bar", "denny": {"sister": "jenny"}}`) 14 | path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano())) 15 | fh, err := os.Create(path) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | _, err = fh.Write(data) 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | return fh 25 | } 26 | 27 | func createFileForTestArray(t *testing.T) *os.File { 28 | data := []byte(`{"foo": "bar", "denny": [{"sister": "jenny"}, {"sister": "jenny1"}]}`) 29 | path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano())) 30 | fh, err := os.Create(path) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | _, err = fh.Write(data) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | return fh 40 | } 41 | 42 | func TestLoadWithGoodFileArray(t *testing.T) { 43 | fh := createFileForTestArray(t) 44 | path := fh.Name() 45 | defer func() { 46 | fh.Close() 47 | os.Remove(path) 48 | }() 49 | 50 | // Create new config 51 | // Load file source 52 | if err := New(path); err != nil { 53 | t.Fatalf("Expected no error but got %v", err) 54 | } 55 | } 56 | 57 | func TestLoadWithGoodFile(t *testing.T) { 58 | fh := createFileForTest(t) 59 | path := fh.Name() 60 | defer func() { 61 | fh.Close() 62 | os.Remove(path) 63 | }() 64 | 65 | // Create new config 66 | // Load file source 67 | if err := New(path); err != nil { 68 | t.Fatalf("Expected no error but got %v", err) 69 | } 70 | } 71 | 72 | func TestReadValue(t *testing.T) { 73 | TestLoadWithGoodFile(t) 74 | ov := "bar" 75 | v := GetString("foo") 76 | if v != ov { 77 | t.Fatalf("Expected bar error but got %v", v) 78 | } 79 | } 80 | func TestLoadWithInvalidFile(t *testing.T) { 81 | fh := createFileForTest(t) 82 | path := fh.Name() 83 | defer func() { 84 | fh.Close() 85 | os.Remove(path) 86 | }() 87 | 88 | // Load file source 89 | err := New(path, 90 | "/i/do/not/exists.json") 91 | 92 | if err == nil { 93 | t.Fatal("Expected error but none !") 94 | } 95 | if !strings.Contains(fmt.Sprintf("%v", err), "/i/do/not/exists.json") { 96 | t.Fatalf("Expected error to contain the unexisting file but got %v", err) 97 | } 98 | } 99 | 100 | func TestWithScanToPointerArray(t *testing.T) { 101 | type Denny struct { 102 | Sister string 103 | } 104 | var ( 105 | v string 106 | denny = []Denny{} 107 | ) 108 | TestLoadWithGoodFileArray(t) 109 | ov := "bar" 110 | 111 | err := Scan(&v, "foo") 112 | 113 | if err != nil { 114 | t.Fatalf("Expect value but got error reading %v", err) 115 | } 116 | if v != ov { 117 | t.Fatalf("Expected bar error but got %v", v) 118 | } 119 | err = Scan(&denny, "denny") 120 | 121 | if err != nil { 122 | t.Fatalf("Expect value but got error reading %v", err) 123 | } 124 | if denny[0].Sister != "jenny" { 125 | t.Fatalf("Expected jenny error but got %v", v) 126 | } 127 | } 128 | 129 | func TestWithScanToPointer(t *testing.T) { 130 | type Denny struct { 131 | Sister string 132 | } 133 | var ( 134 | v string 135 | denny = &Denny{} 136 | ) 137 | TestLoadWithGoodFile(t) 138 | ov := "bar" 139 | 140 | err := Scan(&v, "foo") 141 | 142 | if err != nil { 143 | t.Fatalf("Expect value but got error reading %v", err) 144 | } 145 | if v != ov { 146 | t.Fatalf("Expected bar error but got %v", v) 147 | } 148 | err = Scan(&denny, "denny") 149 | 150 | if err != nil { 151 | t.Fatalf("Expect value but got error reading %v", err) 152 | } 153 | if denny.Sister != "jenny" { 154 | t.Fatalf("Expected jenny error but got %v", v) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /example/brpc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/golang/protobuf/ptypes/empty" 9 | "github.com/opentracing/opentracing-go" 10 | "github.com/uber/jaeger-client-go" 11 | "github.com/uber/jaeger-client-go/zipkin" 12 | "github.com/whatvn/denny" 13 | pb "github.com/whatvn/denny/example/protobuf" 14 | "github.com/whatvn/denny/middleware/grpc" 15 | "github.com/whatvn/denny/middleware/http" 16 | 17 | "github.com/whatvn/denny/naming/redis" 18 | ) 19 | 20 | // grpc 21 | type Hello struct{} 22 | 23 | // groupPath + "/hello/" + "say-hello" 24 | // 25 | func (s *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { 26 | var ( 27 | logger = denny.GetLogger(ctx) 28 | ) 29 | response := &pb.HelloResponse{ 30 | Reply: "hi", 31 | } 32 | 33 | logger.WithField("response", response) 34 | return response, nil 35 | } 36 | 37 | // http get request 38 | // when define grpc method with input is empty.Empty object, denny will consider request as get request 39 | // router will be: 40 | // groupPath + "/hello/" + "say-hello-anonymous" 41 | // rule is rootRoute + "/" kebabCase(serviceName) + "/" kebabCase(methodName) 42 | 43 | func (s *Hello) SayHelloAnonymous(ctx context.Context, in *empty.Empty) (*pb.HelloResponseAnonymous, error) { 44 | 45 | var ( 46 | logger = denny.GetLogger(ctx) 47 | ) 48 | 49 | span, ctx := opentracing.StartSpanFromContext(ctx, "sayHello") 50 | defer span.Finish() 51 | response := &pb.HelloResponseAnonymous{ 52 | Reply: "hoho", 53 | Status: pb.Status_STATUS_SUCCESS, 54 | } 55 | 56 | logger.WithField("response", response) 57 | 58 | return response, nil 59 | } 60 | 61 | type TestController struct { 62 | denny.Controller 63 | } 64 | 65 | func (t *TestController) Handle(ctx *denny.Context) { 66 | ctx.JSON(200, &pb.HelloResponse{ 67 | Reply: "ha", 68 | }) 69 | } 70 | 71 | func newReporterUDP(jaegerAddr string, port int, packetLength int) jaeger.Transport { 72 | hostString := fmt.Sprintf("%s:%d", jaegerAddr, port) 73 | transport, err := jaeger.NewUDPTransport(hostString, packetLength) 74 | if err != nil { 75 | panic(err) 76 | } 77 | return transport 78 | } 79 | func initTracerUDP(jaegerAddr string, port int, packetLength int, serviceName string) (opentracing.Tracer, io.Closer) { 80 | var ( 81 | propagator = zipkin.NewZipkinB3HTTPHeaderPropagator() 82 | ) 83 | 84 | return jaeger.NewTracer( 85 | serviceName, 86 | jaeger.NewConstSampler(true), 87 | jaeger.NewRemoteReporter(newReporterUDP(jaegerAddr, port, packetLength)), 88 | jaeger.TracerOptions.Injector(opentracing.HTTPHeaders, propagator), 89 | jaeger.TracerOptions.Extractor(opentracing.HTTPHeaders, propagator), 90 | jaeger.TracerOptions.ZipkinSharedRPCSpan(true), 91 | ) 92 | } 93 | 94 | func main() { 95 | 96 | // open tracing 97 | tracer, _ := initTracerUDP( 98 | "127.0.0.1", 99 | 6831, 100 | 65000, 101 | "brpc.server.demo", 102 | ) 103 | opentracing.SetGlobalTracer(tracer) 104 | 105 | server := denny.NewServer(true) 106 | server.Use(http.Logger()) 107 | group := server.NewGroup("/hi") 108 | group.Controller("/hi", denny.HttpPost, new(TestController)) 109 | 110 | // setup grpc server 111 | 112 | grpcServer := denny.NewGrpcServer(grpc.ValidatorInterceptor) 113 | pb.RegisterHelloServiceServer(grpcServer, new(Hello)) 114 | server.WithGrpcServer(grpcServer) 115 | // 116 | 117 | //// then http 118 | authorized := server.NewGroup("/") 119 | // http://127.0.0.1:8080/hello/sayhello (POST) 120 | // http://127.0.0.1:8080/hello/sayhelloanonymous (GET) 121 | authorized.BrpcController(&Hello{}) 122 | 123 | // naming registry 124 | registry := redis.New("127.0.0.1:6379", "", "demo.brpc.svc") 125 | server.WithRegistry(registry) 126 | 127 | // start server in dual mode 128 | server.GraceFulStart(":8080") 129 | } 130 | -------------------------------------------------------------------------------- /cache/memory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type memory struct { 9 | count int64 10 | sync.RWMutex 11 | storage *sync.Map 12 | } 13 | 14 | type item struct { 15 | expire time.Duration 16 | createdTime time.Time 17 | value interface{} 18 | } 19 | 20 | func (i *item) life() int64 { 21 | if i.expire == 0 { 22 | return 0 23 | } 24 | return int64(time.Now().Sub(i.createdTime).Seconds()) 25 | } 26 | 27 | func (i *item) isExpire() bool { 28 | return time.Now().Sub(i.createdTime) > i.expire 29 | } 30 | 31 | // Get return value if key exist or nil if it does not 32 | func (c *memory) Get(key string) interface{} { 33 | el, ok := c.storage.Load(key) 34 | if ok { 35 | v := el.(*item) 36 | return v.value 37 | } 38 | return nil 39 | } 40 | 41 | // GetOrElse return value if it exists, else warmup using warmup function 42 | func (c *memory) GetOrElse(key string, wuf func(key string) interface{}, expire ...int64) interface{} { 43 | el, ok := c.storage.Load(key) 44 | if ok { 45 | v := el.(*item) 46 | return v.value 47 | } 48 | if v := wuf(key); v != nil { 49 | var expired int64 = 0 50 | if len(expire) > 0 { 51 | expired = expire[0] 52 | } 53 | c.Set(key, v, expired) 54 | return v 55 | } 56 | return nil 57 | } 58 | 59 | // Set store key in sync map 60 | func (c *memory) Set(key string, val interface{}, expire int64) { 61 | c.storage.Store(key, &item{ 62 | value: val, 63 | createdTime: time.Now(), 64 | expire: time.Duration(expire) * time.Second, 65 | }) 66 | } 67 | 68 | // Get multi will load all values with given keys 69 | // caller has to check request return value against nil befors using 70 | // as this call will not check key existence 71 | func (c *memory) GetMulti(keys []string) []interface{} { 72 | var values []interface{} 73 | for _, k := range keys { 74 | el, _ := c.storage.Load(k) 75 | v, ok := el.(*item) 76 | if ok { 77 | values = append(values, v) 78 | } else { 79 | values = append(values, nil) 80 | } 81 | } 82 | return values 83 | } 84 | 85 | // Delete delete key in map if it exists 86 | func (c *memory) Delete(key string) { 87 | c.storage.Delete(key) 88 | } 89 | 90 | // Incr incr key in map if it exists 91 | func (c *memory) Incr(key string) error { 92 | c.RLock() 93 | defer c.RUnlock() 94 | el, ok := c.storage.Load(key) 95 | if !ok { 96 | return ValueNotExistError 97 | } 98 | 99 | v, ok := el.(*item) 100 | if !ok { 101 | return InvalidValueTypeError 102 | } 103 | 104 | i, ok := v.value.(int64) 105 | if !ok { 106 | return InvalidValueTypeError 107 | } 108 | 109 | c.Set(key, i+1, v.life()) 110 | return nil 111 | } 112 | 113 | // Incr incr key in map if it exists 114 | func (c *memory) Decr(key string) error { 115 | c.Lock() 116 | defer c.Unlock() 117 | el, ok := c.storage.Load(key) 118 | if !ok { 119 | return ValueNotExistError 120 | } 121 | 122 | v, ok := el.(*item) 123 | if !ok { 124 | return InvalidValueTypeError 125 | } 126 | 127 | i, ok := v.value.(int64) 128 | if !ok { 129 | return InvalidValueTypeError 130 | } 131 | 132 | c.Set(key, i-1, v.life()) 133 | return nil 134 | } 135 | 136 | func (c *memory) IsExist(key string) bool { 137 | _, ok := c.storage.Load(key) 138 | return ok 139 | } 140 | 141 | func (c *memory) ClearAll() { 142 | c.storage.Range(func(key, value interface{}) bool { 143 | c.storage.Delete(key) 144 | return true 145 | }) 146 | } 147 | 148 | func (c *memory) runGc(config Config) { 149 | for { 150 | <-time.After(time.Duration(config.GcDuration) * time.Second) 151 | for _, k := range c.expires() { 152 | c.Delete(k) 153 | } 154 | } 155 | } 156 | 157 | func (c *memory) expires() []string { 158 | var keys []string 159 | c.storage.Range(func(k, el interface{}) bool { 160 | v := el.(*item) 161 | if v.expire != 0 { 162 | if v.isExpire() { 163 | keys = append(keys, k.(string)) 164 | } 165 | } 166 | return true 167 | }) 168 | return keys 169 | } 170 | 171 | func NewMemoryCache(cfg Config) Cache { 172 | c := &memory{ 173 | storage: &sync.Map{}, 174 | } 175 | go c.runGc(cfg) 176 | return c 177 | } 178 | -------------------------------------------------------------------------------- /go_config/reader/json/values.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | simple "github.com/bitly/go-simplejson" 11 | "github.com/whatvn/denny/go_config/reader" 12 | "github.com/whatvn/denny/go_config/source" 13 | ) 14 | 15 | type jsonValues struct { 16 | ch *source.ChangeSet 17 | sj *simple.Json 18 | } 19 | 20 | type jsonValue struct { 21 | *simple.Json 22 | } 23 | 24 | func newValues(ch *source.ChangeSet) (reader.Values, error) { 25 | sj := simple.New() 26 | data, _ := reader.ReplaceEnvVars(ch.Data) 27 | if err := sj.UnmarshalJSON(data); err != nil { 28 | sj.SetPath(nil, string(ch.Data)) 29 | } 30 | return &jsonValues{ch, sj}, nil 31 | } 32 | 33 | func newValue(s *simple.Json) reader.Value { 34 | if s == nil { 35 | s = simple.New() 36 | } 37 | return &jsonValue{s} 38 | } 39 | 40 | func (j *jsonValues) Get(path ...string) reader.Value { 41 | return &jsonValue{j.sj.GetPath(path...)} 42 | } 43 | 44 | func (j *jsonValues) Del(path ...string) { 45 | // delete the tree? 46 | if len(path) == 0 { 47 | j.sj = simple.New() 48 | return 49 | } 50 | 51 | if len(path) == 1 { 52 | j.sj.Del(path[0]) 53 | return 54 | } 55 | 56 | vals := j.sj.GetPath(path[:len(path)-1]...) 57 | vals.Del(path[len(path)-1]) 58 | j.sj.SetPath(path[:len(path)-1], vals.Interface()) 59 | return 60 | } 61 | 62 | func (j *jsonValues) Set(val interface{}, path ...string) { 63 | j.sj.SetPath(path, val) 64 | } 65 | 66 | func (j *jsonValues) Bytes() []byte { 67 | b, _ := j.sj.MarshalJSON() 68 | return b 69 | } 70 | 71 | func (j *jsonValues) Map() map[string]interface{} { 72 | m, _ := j.sj.Map() 73 | return m 74 | } 75 | 76 | func (j *jsonValues) Scan(v interface{}) error { 77 | b, err := j.sj.MarshalJSON() 78 | if err != nil { 79 | return err 80 | } 81 | return json.Unmarshal(b, v) 82 | } 83 | 84 | func (j *jsonValues) String() string { 85 | return "json" 86 | } 87 | 88 | func (j *jsonValue) Bool(def bool) bool { 89 | b, err := j.Json.Bool() 90 | if err == nil { 91 | return b 92 | } 93 | 94 | str, ok := j.Interface().(string) 95 | if !ok { 96 | return def 97 | } 98 | 99 | b, err = strconv.ParseBool(str) 100 | if err != nil { 101 | return def 102 | } 103 | 104 | return b 105 | } 106 | 107 | func (j *jsonValue) Int(def int) int { 108 | i, err := j.Json.Int() 109 | if err == nil { 110 | return i 111 | } 112 | 113 | str, ok := j.Interface().(string) 114 | if !ok { 115 | return def 116 | } 117 | 118 | i, err = strconv.Atoi(str) 119 | if err != nil { 120 | return def 121 | } 122 | 123 | return i 124 | } 125 | 126 | func (j *jsonValue) String(def string) string { 127 | return j.Json.MustString(def) 128 | } 129 | 130 | func (j *jsonValue) Float64(def float64) float64 { 131 | f, err := j.Json.Float64() 132 | if err == nil { 133 | return f 134 | } 135 | 136 | str, ok := j.Interface().(string) 137 | if !ok { 138 | return def 139 | } 140 | 141 | f, err = strconv.ParseFloat(str, 64) 142 | if err != nil { 143 | return def 144 | } 145 | 146 | return f 147 | } 148 | 149 | func (j *jsonValue) Duration(def time.Duration) time.Duration { 150 | v, err := j.Json.String() 151 | if err != nil { 152 | return def 153 | } 154 | 155 | value, err := time.ParseDuration(v) 156 | if err != nil { 157 | return def 158 | } 159 | 160 | return value 161 | } 162 | 163 | func (j *jsonValue) StringSlice(def []string) []string { 164 | v, err := j.Json.String() 165 | if err == nil { 166 | sl := strings.Split(v, ",") 167 | if len(sl) > 1 { 168 | return sl 169 | } 170 | } 171 | return j.Json.MustStringArray(def) 172 | } 173 | 174 | func (j *jsonValue) StringMap(def map[string]string) map[string]string { 175 | m, err := j.Json.Map() 176 | if err != nil { 177 | return def 178 | } 179 | 180 | res := map[string]string{} 181 | 182 | for k, v := range m { 183 | res[k] = fmt.Sprintf("%v", v) 184 | } 185 | 186 | return res 187 | } 188 | 189 | func (j *jsonValue) Scan(v interface{}) error { 190 | b, err := j.Json.MarshalJSON() 191 | if err != nil { 192 | return err 193 | } 194 | return json.Unmarshal(b, v) 195 | } 196 | 197 | func (j *jsonValue) Bytes() []byte { 198 | b, err := j.Json.Bytes() 199 | if err != nil { 200 | // try return marshalled 201 | b, err = j.Json.MarshalJSON() 202 | if err != nil { 203 | return []byte{} 204 | } 205 | return b 206 | } 207 | return b 208 | } 209 | -------------------------------------------------------------------------------- /go_config/default.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | "time" 7 | 8 | "github.com/whatvn/denny/go_config/loader" 9 | "github.com/whatvn/denny/go_config/loader/memory" 10 | "github.com/whatvn/denny/go_config/reader" 11 | "github.com/whatvn/denny/go_config/reader/json" 12 | "github.com/whatvn/denny/go_config/source" 13 | ) 14 | 15 | type config struct { 16 | exit chan bool 17 | opts Options 18 | 19 | sync.RWMutex 20 | // the current snapshot 21 | snap *loader.Snapshot 22 | // the current values 23 | vals reader.Values 24 | } 25 | 26 | type watcher struct { 27 | lw loader.Watcher 28 | rd reader.Reader 29 | path []string 30 | value reader.Value 31 | } 32 | 33 | func newConfig(opts ...Option) Config { 34 | options := Options{ 35 | Loader: memory.NewLoader(), 36 | Reader: json.NewReader(), 37 | } 38 | 39 | for _, o := range opts { 40 | o(&options) 41 | } 42 | 43 | options.Loader.Load(options.Source...) 44 | snap, _ := options.Loader.Snapshot() 45 | vals, _ := options.Reader.Values(snap.ChangeSet) 46 | 47 | c := &config{ 48 | exit: make(chan bool), 49 | opts: options, 50 | snap: snap, 51 | vals: vals, 52 | } 53 | 54 | go c.run() 55 | 56 | return c 57 | } 58 | 59 | func (c *config) run() { 60 | watch := func(w loader.Watcher) error { 61 | for { 62 | // get changeset 63 | snap, err := w.Next() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | c.Lock() 69 | 70 | // save 71 | c.snap = snap 72 | 73 | // set values 74 | c.vals, _ = c.opts.Reader.Values(snap.ChangeSet) 75 | 76 | c.Unlock() 77 | } 78 | } 79 | 80 | for { 81 | w, err := c.opts.Loader.Watch() 82 | if err != nil { 83 | time.Sleep(time.Second) 84 | continue 85 | } 86 | 87 | done := make(chan bool) 88 | 89 | // the stop watch func 90 | go func() { 91 | select { 92 | case <-done: 93 | case <-c.exit: 94 | } 95 | w.Stop() 96 | }() 97 | 98 | // block watch 99 | if err := watch(w); err != nil { 100 | // do something better 101 | time.Sleep(time.Second) 102 | } 103 | 104 | // close done chan 105 | close(done) 106 | 107 | // if the config is closed exit 108 | select { 109 | case <-c.exit: 110 | return 111 | default: 112 | } 113 | } 114 | } 115 | 116 | func (c *config) Map() map[string]interface{} { 117 | c.RLock() 118 | defer c.RUnlock() 119 | return c.vals.Map() 120 | } 121 | 122 | func (c *config) Scan(v interface{}) error { 123 | c.RLock() 124 | defer c.RUnlock() 125 | return c.vals.Scan(v) 126 | } 127 | 128 | // sync loads all the sources, calls the parser and updates the config 129 | func (c *config) Sync() error { 130 | if err := c.opts.Loader.Sync(); err != nil { 131 | return err 132 | } 133 | 134 | snap, err := c.opts.Loader.Snapshot() 135 | if err != nil { 136 | return err 137 | } 138 | 139 | c.Lock() 140 | defer c.Unlock() 141 | 142 | c.snap = snap 143 | vals, err := c.opts.Reader.Values(snap.ChangeSet) 144 | if err != nil { 145 | return err 146 | } 147 | c.vals = vals 148 | 149 | return nil 150 | } 151 | 152 | func (c *config) Close() error { 153 | select { 154 | case <-c.exit: 155 | return nil 156 | default: 157 | close(c.exit) 158 | } 159 | return nil 160 | } 161 | 162 | func (c *config) Get(path ...string) reader.Value { 163 | c.RLock() 164 | defer c.RUnlock() 165 | 166 | // did sync actually work? 167 | if c.vals != nil { 168 | return c.vals.Get(path...) 169 | } 170 | 171 | // no value 172 | return newValue() 173 | } 174 | 175 | func (c *config) Bytes() []byte { 176 | c.RLock() 177 | defer c.RUnlock() 178 | 179 | if c.vals == nil { 180 | return []byte{} 181 | } 182 | 183 | return c.vals.Bytes() 184 | } 185 | 186 | func (c *config) Load(sources ...source.Source) error { 187 | if err := c.opts.Loader.Load(sources...); err != nil { 188 | return err 189 | } 190 | 191 | snap, err := c.opts.Loader.Snapshot() 192 | if err != nil { 193 | return err 194 | } 195 | 196 | c.Lock() 197 | defer c.Unlock() 198 | 199 | c.snap = snap 200 | vals, err := c.opts.Reader.Values(snap.ChangeSet) 201 | if err != nil { 202 | return err 203 | } 204 | c.vals = vals 205 | 206 | return nil 207 | } 208 | 209 | func (c *config) Watch(path ...string) (Watcher, error) { 210 | value := c.Get(path...) 211 | 212 | w, err := c.opts.Loader.Watch(path...) 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | return &watcher{ 218 | lw: w, 219 | rd: c.opts.Reader, 220 | path: path, 221 | value: value, 222 | }, nil 223 | } 224 | 225 | func (c *config) String() string { 226 | return "config" 227 | } 228 | 229 | func (w *watcher) Next() (reader.Value, error) { 230 | for { 231 | s, err := w.lw.Next() 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | // only process changes 237 | if bytes.Equal(w.value.Bytes(), s.ChangeSet.Data) { 238 | continue 239 | } 240 | 241 | v, err := w.rd.Values(s.ChangeSet) 242 | if err != nil { 243 | return nil, err 244 | } 245 | 246 | w.value = v.Get() 247 | return w.value, nil 248 | } 249 | } 250 | 251 | func (w *watcher) Stop() error { 252 | return w.lw.Stop() 253 | } 254 | -------------------------------------------------------------------------------- /denny_test.go: -------------------------------------------------------------------------------- 1 | package denny 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/golang/protobuf/ptypes/empty" 8 | "github.com/opentracing/opentracing-go" 9 | "github.com/stretchr/testify/assert" 10 | pb "github.com/whatvn/denny/example/protobuf" 11 | "github.com/whatvn/denny/middleware/grpc" 12 | "github.com/whatvn/denny/naming" 13 | "github.com/whatvn/denny/naming/etcd" 14 | "go.etcd.io/etcd/clientv3" 15 | grpcClient "google.golang.org/grpc" 16 | "google.golang.org/protobuf/encoding/protojson" 17 | "google.golang.org/protobuf/types/known/timestamppb" 18 | "io/ioutil" 19 | "net/http" 20 | "net/http/httptest" 21 | "strings" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | type header struct { 27 | Key string 28 | Value string 29 | } 30 | 31 | var mockTime = time.Date(2021, time.May, 16, 23, 19, 0, 0, time.UTC) 32 | 33 | // grpc 34 | type Hello struct{} 35 | 36 | // groupPath + "/hello/" + "sayhello" 37 | // 38 | func (s *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { 39 | var ( 40 | logger = GetLogger(ctx) 41 | ) 42 | response := &pb.HelloResponse{ 43 | Reply: "hi", 44 | CreatedAt: timestamppb.New(mockTime), 45 | } 46 | 47 | logger.WithField("response", response) 48 | return response, nil 49 | } 50 | 51 | func (s *Hello) SayHelloAnonymous(ctx context.Context, in *empty.Empty) (*pb.HelloResponseAnonymous, error) { 52 | var ( 53 | logger = GetLogger(ctx) 54 | ) 55 | 56 | span, ctx := opentracing.StartSpanFromContext(ctx, "sayHello") 57 | defer span.Finish() 58 | response := &pb.HelloResponseAnonymous{ 59 | Reply: "hoho", 60 | Status: pb.Status_STATUS_SUCCESS, 61 | CreatedAt: timestamppb.New(mockTime), 62 | } 63 | 64 | logger.WithField("response", response) 65 | 66 | return response, nil 67 | } 68 | 69 | func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder { 70 | req := httptest.NewRequest(method, path, nil) 71 | for _, h := range headers { 72 | req.Header.Add(h.Key, h.Value) 73 | } 74 | w := httptest.NewRecorder() 75 | r.ServeHTTP(w, req) 76 | return w 77 | } 78 | 79 | func TestSimpleRequest(t *testing.T) { 80 | signature := "" 81 | server := NewServer() 82 | server.Use(func(c *Context) { 83 | signature += "A" 84 | c.Next() 85 | signature += "B" 86 | }) 87 | server.Use(func(c *Context) { 88 | signature += "C" 89 | }) 90 | server.GET("/", func(c *Context) { 91 | signature += "D" 92 | c.String(http.StatusOK, signature) 93 | }) 94 | server.NoRoute(func(c *Context) { 95 | signature += " X " 96 | }) 97 | server.NoMethod(func(c *Context) { 98 | signature += " XX " 99 | }) 100 | // RUN 101 | w := performRequest(server, "GET", "/") 102 | 103 | out, e := ioutil.ReadAll(w.Result().Body) 104 | 105 | // TEST 106 | assert.Equal(t, nil, e) 107 | assert.Equal(t, "ACD", string(out)) 108 | assert.Equal(t, http.StatusOK, w.Code) 109 | assert.Equal(t, "ACDB", signature) 110 | } 111 | 112 | func TestCustomJsonMarshal(t *testing.T) { 113 | server := NewServer(true) 114 | 115 | // Add your custom JSON response serializer 116 | AddProtoJsonResponseSerializer( 117 | ProtoJsonResponseSerializer(protojson.MarshalOptions{ // You can use the Proto json serializer with protojson.MarshalOptions 118 | Indent: " ", 119 | Multiline: true, 120 | UseProtoNames: false, 121 | EmitUnpopulated: true, 122 | UseEnumNumbers: false, 123 | })) 124 | 125 | // setup grpc server 126 | grpcServer := NewGrpcServer(grpc.ValidatorInterceptor) 127 | pb.RegisterHelloServiceServer(grpcServer, new(Hello)) 128 | server.WithGrpcServer(grpcServer) 129 | // 130 | 131 | //// then http 132 | authorized := server.NewGroup("/") 133 | authorized.BrpcController(&Hello{}) 134 | 135 | // RUN 136 | w := performRequest(server, "GET", "/hello/say-hello-anonymous") 137 | 138 | out, e := ioutil.ReadAll(w.Result().Body) 139 | var res map[string]string 140 | err := json.Unmarshal(out, &res) 141 | if err != nil { 142 | assert.Errorf(t, err, "Error when Unmarshal response") 143 | } 144 | 145 | // TEST 146 | assert.Equal(t, nil, e) 147 | 148 | reply, ok := res["reply"] 149 | if !ok { 150 | assert.Errorf(t, 151 | fmt.Errorf("Not found reply field in response map"), 152 | "Not found reply field in response", res) 153 | } 154 | assert.Equal(t, "hoho", reply) 155 | 156 | status, ok := res["status"] 157 | if !ok { 158 | assert.Errorf(t, 159 | fmt.Errorf("Not found status field in response map"), 160 | "Not found status field in response", res) 161 | } 162 | assert.Equal(t, "STATUS_SUCCESS", status) 163 | 164 | createdAtField, ok := res["createdAt"] // The timestamp is now in RFC3339 format, and the fields are camelCased. 165 | if !ok { 166 | assert.Errorf(t, 167 | fmt.Errorf("Not found status field in response map"), 168 | "Not found status field in response", res) 169 | } 170 | createdAt, err := time.Parse(time.RFC3339, createdAtField) 171 | if err != nil { 172 | assert.Errorf(t, err, "Parse createAt field error", res) 173 | } 174 | assert.Equal(t, mockTime.Equal(createdAt), true) 175 | assert.Equal(t, http.StatusOK, w.Code) 176 | } 177 | 178 | func TestNaming(t *testing.T) { 179 | server := NewServer(true) 180 | 181 | // setup grpc server 182 | grpcServer := NewGrpcServer(grpc.ValidatorInterceptor) 183 | pb.RegisterHelloServiceServer(grpcServer, new(Hello)) 184 | server.WithGrpcServer(grpcServer) 185 | // 186 | 187 | //// then http 188 | authorized := server.NewGroup("/") 189 | authorized.BrpcController(&Hello{}) 190 | 191 | // RUN 192 | clientCfgServer := clientv3.Config{ 193 | Endpoints: strings.Split("58.84.1.31:2379", ";"), 194 | Username: "root", 195 | Password: "phuc12345", 196 | DialTimeout: 15 * time.Second, 197 | } 198 | registryServer := etcd.NewWithClientConfig("bevo.profile", clientCfgServer) 199 | server.WithRegistry(registryServer) 200 | 201 | // start server in dual mode 202 | go server.GraceFulStart(":8081") 203 | 204 | // Run client 205 | clientCfg := clientv3.Config{ 206 | Endpoints: strings.Split("58.84.1.31:2379", ";"), 207 | Username: "root", 208 | Password: "phuc12345", 209 | DialTimeout: 15 * time.Second, 210 | } 211 | registry := etcd.NewResolverWithClientConfig("bevo.profile", clientCfg) 212 | conn, err := grpcClient.Dial(registry.SvcName(), naming.DefaultBalancePolicy(), grpcClient.WithInsecure()) 213 | if err != nil { 214 | panic(err) 215 | } 216 | client := pb.NewHelloServiceClient(conn) 217 | response, err := client.SayHelloAnonymous(context.Background(), &empty.Empty{}) 218 | fmt.Println(response, err) 219 | assert.Equal(t, "hoho", response.Reply) 220 | } 221 | -------------------------------------------------------------------------------- /go_config/loader/memory/memory.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/whatvn/denny/go_config/loader" 12 | "github.com/whatvn/denny/go_config/reader" 13 | "github.com/whatvn/denny/go_config/reader/json" 14 | "github.com/whatvn/denny/go_config/source" 15 | ) 16 | 17 | type memory struct { 18 | exit chan bool 19 | opts loader.Options 20 | 21 | sync.RWMutex 22 | // the current snapshot 23 | snap *loader.Snapshot 24 | // the current values 25 | vals reader.Values 26 | // all the sets 27 | sets []*source.ChangeSet 28 | // all the sources 29 | sources []source.Source 30 | 31 | idx int 32 | watchers map[int]*watcher 33 | } 34 | 35 | type watcher struct { 36 | exit chan bool 37 | path []string 38 | value reader.Value 39 | reader reader.Reader 40 | updates chan reader.Value 41 | } 42 | 43 | func (m *memory) watch(idx int, s source.Source) { 44 | m.Lock() 45 | m.sets = append(m.sets, &source.ChangeSet{Source: s.String()}) 46 | m.Unlock() 47 | 48 | // watches a source for changes 49 | watch := func(idx int, s source.Watcher) error { 50 | for { 51 | // get changeset 52 | cs, err := s.Next() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | m.Lock() 58 | 59 | // save 60 | m.sets[idx] = cs 61 | 62 | // merge sets 63 | set, err := m.opts.Reader.Merge(m.sets...) 64 | if err != nil { 65 | m.Unlock() 66 | return err 67 | } 68 | 69 | // set values 70 | m.vals, _ = m.opts.Reader.Values(set) 71 | m.snap = &loader.Snapshot{ 72 | ChangeSet: set, 73 | Version: fmt.Sprintf("%d", time.Now().Unix()), 74 | } 75 | m.Unlock() 76 | 77 | // send watch updates 78 | m.update() 79 | } 80 | } 81 | 82 | for { 83 | // watch the source 84 | w, err := s.Watch() 85 | if err != nil { 86 | time.Sleep(time.Second) 87 | continue 88 | } 89 | 90 | done := make(chan bool) 91 | 92 | // the stop watch func 93 | go func() { 94 | select { 95 | case <-done: 96 | case <-m.exit: 97 | } 98 | w.Stop() 99 | }() 100 | 101 | // block watch 102 | if err := watch(idx, w); err != nil { 103 | // do something better 104 | time.Sleep(time.Second) 105 | } 106 | 107 | // close done chan 108 | close(done) 109 | 110 | // if the config is closed exit 111 | select { 112 | case <-m.exit: 113 | return 114 | default: 115 | } 116 | } 117 | } 118 | 119 | func (m *memory) loaded() bool { 120 | var loaded bool 121 | m.RLock() 122 | if m.vals != nil { 123 | loaded = true 124 | } 125 | m.RUnlock() 126 | return loaded 127 | } 128 | 129 | // reload reads the sets and creates new values 130 | func (m *memory) reload() error { 131 | m.Lock() 132 | 133 | // merge sets 134 | set, err := m.opts.Reader.Merge(m.sets...) 135 | if err != nil { 136 | m.Unlock() 137 | return err 138 | } 139 | 140 | // set values 141 | m.vals, _ = m.opts.Reader.Values(set) 142 | m.snap = &loader.Snapshot{ 143 | ChangeSet: set, 144 | Version: fmt.Sprintf("%d", time.Now().Unix()), 145 | } 146 | 147 | m.Unlock() 148 | 149 | // update watchers 150 | m.update() 151 | 152 | return nil 153 | } 154 | 155 | func (m *memory) update() { 156 | var watchers []*watcher 157 | 158 | m.RLock() 159 | for _, w := range m.watchers { 160 | watchers = append(watchers, w) 161 | } 162 | m.RUnlock() 163 | 164 | for _, w := range watchers { 165 | select { 166 | case w.updates <- m.vals.Get(w.path...): 167 | default: 168 | } 169 | } 170 | } 171 | 172 | // Snapshot returns a snapshot of the current loaded config 173 | func (m *memory) Snapshot() (*loader.Snapshot, error) { 174 | if m.loaded() { 175 | m.RLock() 176 | snap := loader.Copy(m.snap) 177 | m.RUnlock() 178 | return snap, nil 179 | } 180 | 181 | // not loaded, sync 182 | if err := m.Sync(); err != nil { 183 | return nil, err 184 | } 185 | 186 | // make copy 187 | m.RLock() 188 | snap := loader.Copy(m.snap) 189 | m.RUnlock() 190 | 191 | return snap, nil 192 | } 193 | 194 | // Sync loads all the sources, calls the parser and updates the config 195 | func (m *memory) Sync() error { 196 | var sets []*source.ChangeSet 197 | 198 | m.Lock() 199 | 200 | // read the source 201 | var gerr []string 202 | 203 | for _, source := range m.sources { 204 | ch, err := source.Read() 205 | if err != nil { 206 | gerr = append(gerr, err.Error()) 207 | continue 208 | } 209 | sets = append(sets, ch) 210 | } 211 | 212 | // merge sets 213 | set, err := m.opts.Reader.Merge(sets...) 214 | if err != nil { 215 | m.Unlock() 216 | return err 217 | } 218 | 219 | // set values 220 | vals, err := m.opts.Reader.Values(set) 221 | if err != nil { 222 | m.Unlock() 223 | return err 224 | } 225 | m.vals = vals 226 | m.snap = &loader.Snapshot{ 227 | ChangeSet: set, 228 | Version: fmt.Sprintf("%d", time.Now().Unix()), 229 | } 230 | 231 | m.Unlock() 232 | 233 | // update watchers 234 | m.update() 235 | 236 | if len(gerr) > 0 { 237 | return fmt.Errorf("source loading errors: %s", strings.Join(gerr, "\n")) 238 | } 239 | 240 | return nil 241 | } 242 | 243 | func (m *memory) Close() error { 244 | select { 245 | case <-m.exit: 246 | return nil 247 | default: 248 | close(m.exit) 249 | } 250 | return nil 251 | } 252 | 253 | func (m *memory) Get(path ...string) (reader.Value, error) { 254 | if !m.loaded() { 255 | if err := m.Sync(); err != nil { 256 | return nil, err 257 | } 258 | } 259 | 260 | m.Lock() 261 | defer m.Unlock() 262 | 263 | // did sync actually work? 264 | if m.vals != nil { 265 | return m.vals.Get(path...), nil 266 | } 267 | 268 | // assuming vals is nil 269 | // create new vals 270 | 271 | ch := m.snap.ChangeSet 272 | 273 | // we are truly screwed, trying to load in a hacked way 274 | v, err := m.opts.Reader.Values(ch) 275 | if err != nil { 276 | return nil, err 277 | } 278 | 279 | // lets set it just because 280 | m.vals = v 281 | 282 | if m.vals != nil { 283 | return m.vals.Get(path...), nil 284 | } 285 | 286 | // ok we're going hardcore now 287 | return nil, errors.New("no values") 288 | } 289 | 290 | func (m *memory) Load(sources ...source.Source) error { 291 | var gerrors []string 292 | 293 | for _, source := range sources { 294 | set, err := source.Read() 295 | if err != nil { 296 | gerrors = append(gerrors, 297 | fmt.Sprintf("error loading source %s: %v", 298 | source, 299 | err)) 300 | // continue processing 301 | continue 302 | } 303 | m.Lock() 304 | m.sources = append(m.sources, source) 305 | m.sets = append(m.sets, set) 306 | idx := len(m.sets) - 1 307 | m.Unlock() 308 | go m.watch(idx, source) 309 | } 310 | 311 | if err := m.reload(); err != nil { 312 | gerrors = append(gerrors, err.Error()) 313 | } 314 | 315 | // Return errors 316 | if len(gerrors) != 0 { 317 | return errors.New(strings.Join(gerrors, "\n")) 318 | } 319 | return nil 320 | } 321 | 322 | func (m *memory) Watch(path ...string) (loader.Watcher, error) { 323 | value, err := m.Get(path...) 324 | if err != nil { 325 | return nil, err 326 | } 327 | 328 | m.Lock() 329 | 330 | w := &watcher{ 331 | exit: make(chan bool), 332 | path: path, 333 | value: value, 334 | reader: m.opts.Reader, 335 | updates: make(chan reader.Value, 1), 336 | } 337 | 338 | id := m.idx 339 | m.watchers[id] = w 340 | m.idx++ 341 | 342 | m.Unlock() 343 | 344 | go func() { 345 | <-w.exit 346 | m.Lock() 347 | delete(m.watchers, id) 348 | m.Unlock() 349 | }() 350 | 351 | return w, nil 352 | } 353 | 354 | func (m *memory) String() string { 355 | return "memory" 356 | } 357 | 358 | func (w *watcher) Next() (*loader.Snapshot, error) { 359 | for { 360 | select { 361 | case <-w.exit: 362 | return nil, errors.New("watcher stopped") 363 | case v := <-w.updates: 364 | if bytes.Equal(w.value.Bytes(), v.Bytes()) { 365 | continue 366 | } 367 | w.value = v 368 | 369 | cs := &source.ChangeSet{ 370 | Data: v.Bytes(), 371 | Format: w.reader.String(), 372 | Source: "memory", 373 | Timestamp: time.Now(), 374 | } 375 | cs.Sum() 376 | 377 | return &loader.Snapshot{ 378 | ChangeSet: cs, 379 | Version: fmt.Sprintf("%d", time.Now().Unix()), 380 | }, nil 381 | } 382 | } 383 | } 384 | 385 | func (w *watcher) Stop() error { 386 | select { 387 | case <-w.exit: 388 | default: 389 | close(w.exit) 390 | } 391 | return nil 392 | } 393 | 394 | func NewLoader(opts ...loader.Option) loader.Loader { 395 | options := loader.Options{ 396 | Reader: json.NewReader(), 397 | } 398 | 399 | for _, o := range opts { 400 | o(&options) 401 | } 402 | 403 | m := &memory{ 404 | exit: make(chan bool), 405 | opts: options, 406 | watchers: make(map[int]*watcher), 407 | sources: options.Source, 408 | } 409 | 410 | for i, s := range options.Source { 411 | go m.watch(i, s) 412 | } 413 | 414 | return m 415 | } 416 | -------------------------------------------------------------------------------- /example/protobuf/hello.pb.validate.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-validate. DO NOT EDIT. 2 | // source: protobuf/hello.proto 3 | 4 | package protobuf 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "net/mail" 12 | "net/url" 13 | "regexp" 14 | "strings" 15 | "time" 16 | "unicode/utf8" 17 | 18 | "google.golang.org/protobuf/types/known/anypb" 19 | ) 20 | 21 | // ensure the imports are used 22 | var ( 23 | _ = bytes.MinRead 24 | _ = errors.New("") 25 | _ = fmt.Print 26 | _ = utf8.UTFMax 27 | _ = (*regexp.Regexp)(nil) 28 | _ = (*strings.Reader)(nil) 29 | _ = net.IPv4len 30 | _ = time.Duration(0) 31 | _ = (*url.URL)(nil) 32 | _ = (*mail.Address)(nil) 33 | _ = anypb.Any{} 34 | ) 35 | 36 | // Validate checks the field values on HelloRequest with the rules defined in 37 | // the proto definition for this message. If any rules are violated, an error 38 | // is returned. When asked to return all errors, validation continues after 39 | // first violation, and the result is a list of violation errors wrapped in 40 | // HelloRequestMultiError, or nil if none found. Otherwise, only the first 41 | // error is returned, if any. 42 | func (m *HelloRequest) Validate(all bool) error { 43 | if m == nil { 44 | return nil 45 | } 46 | 47 | var errors []error 48 | 49 | if utf8.RuneCountInString(m.GetGreeting()) < 1 { 50 | err := HelloRequestValidationError{ 51 | field: "Greeting", 52 | reason: "value length must be at least 1 runes", 53 | } 54 | if !all { 55 | return err 56 | } 57 | errors = append(errors, err) 58 | } 59 | 60 | if len(errors) > 0 { 61 | return HelloRequestMultiError(errors) 62 | } 63 | return nil 64 | } 65 | 66 | // HelloRequestMultiError is an error wrapping multiple validation errors 67 | // returned by HelloRequest.Validate(true) if the designated constraints 68 | // aren't met. 69 | type HelloRequestMultiError []error 70 | 71 | // Error returns a concatenation of all the error messages it wraps. 72 | func (m HelloRequestMultiError) Error() string { 73 | var msgs []string 74 | for _, err := range m { 75 | msgs = append(msgs, err.Error()) 76 | } 77 | return strings.Join(msgs, "; ") 78 | } 79 | 80 | // AllErrors returns a list of validation violation errors. 81 | func (m HelloRequestMultiError) AllErrors() []error { return m } 82 | 83 | // HelloRequestValidationError is the validation error returned by 84 | // HelloRequest.Validate if the designated constraints aren't met. 85 | type HelloRequestValidationError struct { 86 | field string 87 | reason string 88 | cause error 89 | key bool 90 | } 91 | 92 | // Field function returns field value. 93 | func (e HelloRequestValidationError) Field() string { return e.field } 94 | 95 | // Reason function returns reason value. 96 | func (e HelloRequestValidationError) Reason() string { return e.reason } 97 | 98 | // Cause function returns cause value. 99 | func (e HelloRequestValidationError) Cause() error { return e.cause } 100 | 101 | // Key function returns key value. 102 | func (e HelloRequestValidationError) Key() bool { return e.key } 103 | 104 | // ErrorName returns error name. 105 | func (e HelloRequestValidationError) ErrorName() string { return "HelloRequestValidationError" } 106 | 107 | // Error satisfies the builtin error interface 108 | func (e HelloRequestValidationError) Error() string { 109 | cause := "" 110 | if e.cause != nil { 111 | cause = fmt.Sprintf(" | caused by: %v", e.cause) 112 | } 113 | 114 | key := "" 115 | if e.key { 116 | key = "key for " 117 | } 118 | 119 | return fmt.Sprintf( 120 | "invalid %sHelloRequest.%s: %s%s", 121 | key, 122 | e.field, 123 | e.reason, 124 | cause) 125 | } 126 | 127 | var _ error = HelloRequestValidationError{} 128 | 129 | var _ interface { 130 | Field() string 131 | Reason() string 132 | Key() bool 133 | Cause() error 134 | ErrorName() string 135 | } = HelloRequestValidationError{} 136 | 137 | // Validate checks the field values on HelloResponse with the rules defined in 138 | // the proto definition for this message. If any rules are violated, an error 139 | // is returned. When asked to return all errors, validation continues after 140 | // first violation, and the result is a list of violation errors wrapped in 141 | // HelloResponseMultiError, or nil if none found. Otherwise, only the first 142 | // error is returned, if any. 143 | func (m *HelloResponse) Validate(all bool) error { 144 | if m == nil { 145 | return nil 146 | } 147 | 148 | var errors []error 149 | 150 | // no validation rules for Reply 151 | 152 | if v, ok := interface{}(m.GetCreatedAt()).(interface{ Validate(bool) error }); ok { 153 | if err := v.Validate(all); err != nil { 154 | err = HelloResponseValidationError{ 155 | field: "CreatedAt", 156 | reason: "embedded message failed validation", 157 | cause: err, 158 | } 159 | if !all { 160 | return err 161 | } 162 | errors = append(errors, err) 163 | } 164 | } 165 | 166 | if len(errors) > 0 { 167 | return HelloResponseMultiError(errors) 168 | } 169 | return nil 170 | } 171 | 172 | // HelloResponseMultiError is an error wrapping multiple validation errors 173 | // returned by HelloResponse.Validate(true) if the designated constraints 174 | // aren't met. 175 | type HelloResponseMultiError []error 176 | 177 | // Error returns a concatenation of all the error messages it wraps. 178 | func (m HelloResponseMultiError) Error() string { 179 | var msgs []string 180 | for _, err := range m { 181 | msgs = append(msgs, err.Error()) 182 | } 183 | return strings.Join(msgs, "; ") 184 | } 185 | 186 | // AllErrors returns a list of validation violation errors. 187 | func (m HelloResponseMultiError) AllErrors() []error { return m } 188 | 189 | // HelloResponseValidationError is the validation error returned by 190 | // HelloResponse.Validate if the designated constraints aren't met. 191 | type HelloResponseValidationError struct { 192 | field string 193 | reason string 194 | cause error 195 | key bool 196 | } 197 | 198 | // Field function returns field value. 199 | func (e HelloResponseValidationError) Field() string { return e.field } 200 | 201 | // Reason function returns reason value. 202 | func (e HelloResponseValidationError) Reason() string { return e.reason } 203 | 204 | // Cause function returns cause value. 205 | func (e HelloResponseValidationError) Cause() error { return e.cause } 206 | 207 | // Key function returns key value. 208 | func (e HelloResponseValidationError) Key() bool { return e.key } 209 | 210 | // ErrorName returns error name. 211 | func (e HelloResponseValidationError) ErrorName() string { return "HelloResponseValidationError" } 212 | 213 | // Error satisfies the builtin error interface 214 | func (e HelloResponseValidationError) Error() string { 215 | cause := "" 216 | if e.cause != nil { 217 | cause = fmt.Sprintf(" | caused by: %v", e.cause) 218 | } 219 | 220 | key := "" 221 | if e.key { 222 | key = "key for " 223 | } 224 | 225 | return fmt.Sprintf( 226 | "invalid %sHelloResponse.%s: %s%s", 227 | key, 228 | e.field, 229 | e.reason, 230 | cause) 231 | } 232 | 233 | var _ error = HelloResponseValidationError{} 234 | 235 | var _ interface { 236 | Field() string 237 | Reason() string 238 | Key() bool 239 | Cause() error 240 | ErrorName() string 241 | } = HelloResponseValidationError{} 242 | 243 | // Validate checks the field values on HelloResponseAnonymous with the rules 244 | // defined in the proto definition for this message. If any rules are 245 | // violated, an error is returned. When asked to return all errors, validation 246 | // continues after first violation, and the result is a list of violation 247 | // errors wrapped in HelloResponseAnonymousMultiError, or nil if none found. 248 | // Otherwise, only the first error is returned, if any. 249 | func (m *HelloResponseAnonymous) Validate(all bool) error { 250 | if m == nil { 251 | return nil 252 | } 253 | 254 | var errors []error 255 | 256 | // no validation rules for Reply 257 | 258 | // no validation rules for Status 259 | 260 | if v, ok := interface{}(m.GetCreatedAt()).(interface{ Validate(bool) error }); ok { 261 | if err := v.Validate(all); err != nil { 262 | err = HelloResponseAnonymousValidationError{ 263 | field: "CreatedAt", 264 | reason: "embedded message failed validation", 265 | cause: err, 266 | } 267 | if !all { 268 | return err 269 | } 270 | errors = append(errors, err) 271 | } 272 | } 273 | 274 | if len(errors) > 0 { 275 | return HelloResponseAnonymousMultiError(errors) 276 | } 277 | return nil 278 | } 279 | 280 | // HelloResponseAnonymousMultiError is an error wrapping multiple validation 281 | // errors returned by HelloResponseAnonymous.Validate(true) if the designated 282 | // constraints aren't met. 283 | type HelloResponseAnonymousMultiError []error 284 | 285 | // Error returns a concatenation of all the error messages it wraps. 286 | func (m HelloResponseAnonymousMultiError) Error() string { 287 | var msgs []string 288 | for _, err := range m { 289 | msgs = append(msgs, err.Error()) 290 | } 291 | return strings.Join(msgs, "; ") 292 | } 293 | 294 | // AllErrors returns a list of validation violation errors. 295 | func (m HelloResponseAnonymousMultiError) AllErrors() []error { return m } 296 | 297 | // HelloResponseAnonymousValidationError is the validation error returned by 298 | // HelloResponseAnonymous.Validate if the designated constraints aren't met. 299 | type HelloResponseAnonymousValidationError struct { 300 | field string 301 | reason string 302 | cause error 303 | key bool 304 | } 305 | 306 | // Field function returns field value. 307 | func (e HelloResponseAnonymousValidationError) Field() string { return e.field } 308 | 309 | // Reason function returns reason value. 310 | func (e HelloResponseAnonymousValidationError) Reason() string { return e.reason } 311 | 312 | // Cause function returns cause value. 313 | func (e HelloResponseAnonymousValidationError) Cause() error { return e.cause } 314 | 315 | // Key function returns key value. 316 | func (e HelloResponseAnonymousValidationError) Key() bool { return e.key } 317 | 318 | // ErrorName returns error name. 319 | func (e HelloResponseAnonymousValidationError) ErrorName() string { 320 | return "HelloResponseAnonymousValidationError" 321 | } 322 | 323 | // Error satisfies the builtin error interface 324 | func (e HelloResponseAnonymousValidationError) Error() string { 325 | cause := "" 326 | if e.cause != nil { 327 | cause = fmt.Sprintf(" | caused by: %v", e.cause) 328 | } 329 | 330 | key := "" 331 | if e.key { 332 | key = "key for " 333 | } 334 | 335 | return fmt.Sprintf( 336 | "invalid %sHelloResponseAnonymous.%s: %s%s", 337 | key, 338 | e.field, 339 | e.reason, 340 | cause) 341 | } 342 | 343 | var _ error = HelloResponseAnonymousValidationError{} 344 | 345 | var _ interface { 346 | Field() string 347 | Reason() string 348 | Key() bool 349 | Cause() error 350 | ErrorName() string 351 | } = HelloResponseAnonymousValidationError{} 352 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # denny 2 | 3 | common http/grpc server which simplify request handling and logging by combining libraries, framework to be able to 4 | 5 | - support both http and grpc in one controller, write once, support both protocol. 6 | See [example](https://github.com/whatvn/denny/blob/master/example/brpc.go) 7 | - support class base request controller, one controller for one handler, **or** you can describe your service in grpc 8 | and implement grpc service, denny will then support HTTP/gRPC when you start it in brpc mode, 9 | see [example](https://github.com/whatvn/denny/blob/master/example/brpc.go) 10 | - make cache usage simpler 11 | - use open tracing 12 | - make config reader simpler 13 | - make logger attached in request context, log should be showed as steps and in only one line for every request 14 | 15 | `denny` is not a http/grpc server from scratch, by now it's based on [gin framework](https://github.com/gin-gonic/gin) 16 | and google grpc server, with help of Golang reflection to support both protocol while requires user just minimum about 17 | of work. Eq: you just need to write your service in grpc, `denny` will also support HTTP for your implementation. 18 | 19 | `denny` is different from [grpc gateway](https://github.com/grpc-ecosystem/grpc-gateway), grpc gateway uses code 20 | generation to generate http proxy call, a request to http enpoint of grpc gateway will also trigger another grpc call to 21 | your service. it's http proxy to grpc, with `denny`, a call to http will only invoke the code you wrote, does not 22 | trigger grpc call. It applies also for grpc call. Because of that, using grpc your service has to start with 2 services 23 | port, 1 for http and 1 for grpc, `denny` need only one for both protocol. 24 | 25 | It also borrows many component from well known libraries (go-config, beego, logrus...). 26 | 27 | ## usage example 28 | 29 | ### Register discoverable grpc service 30 | 31 | #### using etcd as naming storage 32 | 33 | ```go 34 | 35 | //server 36 | func main() { 37 | 38 | server := denny.NewServer(true) 39 | // setup grpc server 40 | 41 | grpcServer := denny.NewGrpcServer() 42 | pb.RegisterHelloServiceServer(grpcServer, new(Hello)) 43 | server.WithGrpcServer(grpcServer) 44 | 45 | registry := etcd.New("127.0.0.1:7379", "demo.brpc.svc") 46 | server.WithRegistry(registry) 47 | 48 | // start server in dual mode 49 | server.GraceFulStart(":8081") 50 | } 51 | ``` 52 | 53 | ```go 54 | // client 55 | 56 | package main 57 | 58 | import ( 59 | "context" 60 | "fmt" 61 | "github.com/golang/protobuf/ptypes/empty" 62 | "github.com/whatvn/denny/example/protobuf" 63 | "github.com/whatvn/denny/naming/etcd" 64 | "github.com/whatvn/denny/naming" 65 | "google.golang.org/grpc" 66 | ) 67 | 68 | func main() { 69 | 70 | registry := etcd.NewResolver("127.0.0.1:7379", "demo.brpc.svc") 71 | conn, err := grpc.Dial(registry.SvcName(), naming.DefaultBalancePolicy(), grpc.WithInsecure()) 72 | if err != nil { 73 | panic(err) 74 | } 75 | client := pb.NewHelloServiceClient(conn) 76 | response, err := client.SayHelloAnonymous(context.Background(), &empty.Empty{}) 77 | fmt.Println(response, err) 78 | } 79 | ``` 80 | 81 | ### using redis as naming storage 82 | 83 | ```go 84 | 85 | //server 86 | func main() { 87 | 88 | server := denny.NewServer(true) 89 | // setup grpc server 90 | 91 | grpcServer := denny.NewGrpcServer() 92 | pb.RegisterHelloServiceServer(grpcServer, new(Hello)) 93 | server.WithGrpcServer(grpcServer) 94 | 95 | registry := redis.New("127.0.0.1:6379","", "demo.brpc.svc") 96 | server.WithRegistry(registry) 97 | 98 | // start server in dual mode 99 | server.GraceFulStart(":8081") 100 | } 101 | ``` 102 | 103 | ```go 104 | // client 105 | 106 | package main 107 | 108 | import ( 109 | "context" 110 | "fmt" 111 | "github.com/golang/protobuf/ptypes/empty" 112 | "github.com/whatvn/denny/example/protobuf" 113 | "github.com/whatvn/denny/naming/redis" 114 | "github.com/whatvn/denny/naming" 115 | "google.golang.org/grpc" 116 | ) 117 | 118 | func main() { 119 | 120 | registry := redis.NewResolver("127.0.0.1:7379", "", "demo.brpc.svc") 121 | conn, err := grpc.Dial(registry.SvcName(), naming.DefaultBalancePolicy(), grpc.WithInsecure()) 122 | if err != nil { 123 | panic(err) 124 | } 125 | client := pb.NewHelloServiceClient(conn) 126 | response, err := client.SayHelloAnonymous(context.Background(), &empty.Empty{}) 127 | fmt.Println(response, err) 128 | } 129 | ``` 130 | 131 | ### Write grpc code but support both http/grpc 132 | 133 | ```go 134 | package main 135 | 136 | import ( 137 | "context" 138 | "fmt" 139 | "github.com/golang/protobuf/ptypes/empty" 140 | "github.com/opentracing/opentracing-go" 141 | "github.com/uber/jaeger-client-go" 142 | "github.com/uber/jaeger-client-go/zipkin" 143 | "github.com/whatvn/denny" 144 | pb "github.com/whatvn/denny/example/protobuf" 145 | "github.com/whatvn/denny/middleware/http" 146 | "github.com/whatvn/denny/naming/etcd" 147 | "io" 148 | ) 149 | 150 | // grpc 151 | type Hello struct{} 152 | 153 | // groupPath + "/hello/" + "say-hello" 154 | // 155 | func (s *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { 156 | var ( 157 | logger = denny.GetLogger(ctx) 158 | ) 159 | response := &pb.HelloResponse{ 160 | Reply: "hi", 161 | } 162 | 163 | logger.WithField("response", response) 164 | return response, nil 165 | } 166 | 167 | // http get request 168 | // when define grpc method with input is empty.Empty object, denny will consider request as get request 169 | // router will be: 170 | // groupPath + "/hello/" + "say-hello-anonymous" 171 | // rule is rootRoute + "/" kebabCase(serviceName) + "/" kebabCase(methodName) 172 | 173 | func (s *Hello) SayHelloAnonymous(ctx context.Context, in *empty.Empty) (*pb.HelloResponse, error) { 174 | 175 | var ( 176 | logger = denny.GetLogger(ctx) 177 | ) 178 | 179 | span, ctx := opentracing.StartSpanFromContext(ctx, "sayHello") 180 | defer span.Finish() 181 | response := &pb.HelloResponse{ 182 | Reply: "ha", 183 | } 184 | 185 | logger.WithField("response", response) 186 | 187 | return response, nil 188 | } 189 | 190 | type TestController struct { 191 | denny.Controller 192 | } 193 | 194 | func (t *TestController) Handle(ctx *denny.Context) { 195 | ctx.JSON(200, &pb.HelloResponse{ 196 | Reply: "ha", 197 | }) 198 | } 199 | 200 | func newReporterUDP(jaegerAddr string, port int, packetLength int) jaeger.Transport { 201 | hostString := fmt.Sprintf("%s:%d", jaegerAddr, port) 202 | transport, err := jaeger.NewUDPTransport(hostString, packetLength) 203 | if err != nil { 204 | panic(err) 205 | } 206 | return transport 207 | } 208 | func initTracerUDP(jaegerAddr string, port int, packetLength int, serviceName string) (opentracing.Tracer, io.Closer) { 209 | var ( 210 | propagator = zipkin.NewZipkinB3HTTPHeaderPropagator() 211 | ) 212 | 213 | return jaeger.NewTracer( 214 | serviceName, 215 | jaeger.NewConstSampler(true), 216 | jaeger.NewRemoteReporter(newReporterUDP(jaegerAddr, port, packetLength)), 217 | jaeger.TracerOptions.Injector(opentracing.HTTPHeaders, propagator), 218 | jaeger.TracerOptions.Extractor(opentracing.HTTPHeaders, propagator), 219 | jaeger.TracerOptions.ZipkinSharedRPCSpan(true), 220 | ) 221 | } 222 | 223 | func main() { 224 | 225 | // open tracing 226 | tracer, _ := initTracerUDP( 227 | "127.0.0.1", 228 | 6831, 229 | 65000, 230 | "brpc.server.demo", 231 | ) 232 | opentracing.SetGlobalTracer(tracer) 233 | 234 | server := denny.NewServer(true) 235 | server.Use(http.Logger()) 236 | group := server.NewGroup("/hi") 237 | group.Controller("/hi", denny.HttpPost, new(TestController)) 238 | 239 | // setup grpc server 240 | 241 | grpcServer := denny.NewGrpcServer() 242 | pb.RegisterHelloServiceServer(grpcServer, new(Hello)) 243 | server.WithGrpcServer(grpcServer) 244 | // 245 | 246 | //// then http 247 | authorized := server.NewGroup("/") 248 | // http://127.0.0.1:8080/hello/sayhello (POST) 249 | // http://127.0.0.1:8080/hello/sayhelloanonymous (GET) 250 | authorized.BrpcController(&Hello{}) 251 | 252 | // naming registry 253 | registry := etcd.New("127.0.0.1:7379", "demo.brpc.svc") 254 | server.WithRegistry(registry) 255 | 256 | // start server in dual mode 257 | server.GraceFulStart(":8081") 258 | } 259 | 260 | ``` 261 | 262 | For customizing response protobuf JSON Serialization. The Golang JSON serializer doesn’t deal well with Protobuf. 263 | Instead, you should use protojson: 264 | 265 | ```go 266 | package main 267 | 268 | import ( 269 | "context" 270 | 271 | "github.com/golang/protobuf/ptypes/empty" 272 | "github.com/opentracing/opentracing-go" 273 | "github.com/whatvn/denny" 274 | pb "github.com/whatvn/denny/example/protobuf" 275 | "github.com/whatvn/denny/middleware/grpc" 276 | "google.golang.org/protobuf/encoding/protojson" 277 | "google.golang.org/protobuf/types/known/timestamppb" 278 | ) 279 | 280 | // grpc 281 | type Hello struct{} 282 | 283 | // groupPath + "/hello/" + "sayhello" 284 | // 285 | func (s *Hello) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { 286 | var ( 287 | logger = denny.GetLogger(ctx) 288 | ) 289 | response := &pb.HelloResponse{ 290 | Reply: "hi", 291 | CreatedAt: timestamppb.Now(), 292 | } 293 | 294 | logger.WithField("response", response) 295 | return response, nil 296 | } 297 | 298 | func (s *Hello) SayHelloAnonymous(ctx context.Context, in *empty.Empty) (*pb.HelloResponseAnonymous, error) { 299 | var ( 300 | logger = denny.GetLogger(ctx) 301 | ) 302 | 303 | span, ctx := opentracing.StartSpanFromContext(ctx, "sayHello") 304 | defer span.Finish() 305 | response := &pb.HelloResponseAnonymous{ 306 | Reply: "hoho", 307 | Status: pb.Status_STATUS_FAIL, 308 | CreatedAt: timestamppb.Now(), 309 | } 310 | 311 | logger.WithField("response", response) 312 | 313 | return response, nil 314 | } 315 | 316 | func main() { 317 | server := denny.NewServer(true) 318 | // Add your custom JSON response serializer 319 | denny.AddProtoJsonResponseSerializer( 320 | denny.ProtoJsonResponseSerializer( // You can use the Proto json serializer with protojson.MarshalOptions or 321 | protojson.MarshalOptions{ // create your own one with denny.ProtoJsonSerializer function type 322 | Indent: " ", // Multiline and Indent — print the message on multiple lines, using the provided indent. 323 | Multiline: true, 324 | UseProtoNames: false, // Effectively makes the JSON keys snake_cased instead of camelCased. 325 | EmitUnpopulated: true, // Explicitly include unpopulated values in the output 326 | UseEnumNumbers: false, // print enums as int instead of their .String() representations. This is what the regular JSON marshaller gave us earlier. 327 | })) 328 | 329 | // setup grpc server 330 | grpcServer := denny.NewGrpcServer(grpc.ValidatorInterceptor) 331 | pb.RegisterHelloServiceServer(grpcServer, new(Hello)) 332 | server.WithGrpcServer(grpcServer) 333 | 334 | //// then http 335 | authorized := server.NewGroup("/") 336 | authorized.BrpcController(&Hello{}) 337 | 338 | server.GraceFulStart(":8080") 339 | } 340 | ``` 341 | 342 | When send request to the server 343 | 344 | ```shell 345 | # Without ProtoJsonResponseSerializer 346 | curl http://localhost:8080/hello/say-hello-anonymous 347 | 348 | {"reply":"hoho","status":1,"created_at":{"seconds":1621181088,"nanos":984488000}} 349 | 350 | # With ProtoJsonResponseSerializer 351 | curl http://localhost:8080/hello/say-hello-anonymous 352 | 353 | {"reply":"hoho","status":"STATUS_FAIL","createdAt":"2021-05-16T16:05:59.312303Z"} 354 | ``` 355 | 356 | ### setting up simple http request handler 357 | 358 | ```go 359 | 360 | package main 361 | 362 | import ( 363 | "github.com/whatvn/denny" 364 | "github.com/whatvn/denny/middleware/http" 365 | ) 366 | 367 | type xController struct { 368 | denny.Controller 369 | } 370 | 371 | // define handle function for controller 372 | func (x xController) Handle(ctx *denny.Context) { 373 | var ( 374 | logger = denny.GetLogger(ctx) 375 | ) 376 | logger.AddLog("receive request") 377 | var str = "hello" 378 | logger.AddLog("do more thing") // logger middleware will log automatically when request finished 379 | str += " world" 380 | ctx.Writer.Write([]byte(str)) 381 | } 382 | 383 | func main() { 384 | server := denny.NewServer() 385 | server.WithMiddleware(http.Logger()) 386 | server.Controller("/", denny.HttpGet, &xController{}) 387 | server.GraceFulStart() 388 | } 389 | 390 | ``` 391 | 392 | ### Reading config 393 | 394 | ```go 395 | 396 | package main 397 | 398 | import ( 399 | "fmt" 400 | "github.com/whatvn/denny/config" 401 | "os" 402 | "path/filepath" 403 | "time" 404 | ) 405 | 406 | func configFile() (*os.File, error) { 407 | data := []byte(`{"foo": "bar", "denny": {"sister": "jenny"}}`) 408 | path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano())) 409 | fh, err := os.Create(path) 410 | if err != nil { 411 | return nil, err 412 | } 413 | _, err = fh.Write(data) 414 | if err != nil { 415 | return nil, err 416 | } 417 | 418 | return fh, nil 419 | } 420 | 421 | func main() { 422 | f, err := configFile() 423 | if err != nil { 424 | fmt.Println(err) 425 | } 426 | // read config from file 427 | config.New(f.Name()) 428 | fmt.Println(config.GetString("foo")) 429 | fmt.Println(config.GetString("denny", "sister")) 430 | 431 | // config from evn takes higher priority 432 | os.Setenv("foo", "barbar") 433 | os.Setenv("denny_sister", "Jenny") 434 | config.Reload() 435 | fmt.Println(config.GetString("foo")) 436 | fmt.Println(config.GetString("denny", "sister")) 437 | } 438 | ``` 439 | 440 | # limit 441 | 442 | Denny uses etcd as naming registry, but etcd packaging is somewhat complicated, new version links to old version, old 443 | version links to older version which is very difficult to optimize import, so currently it use a fork version of 444 | etcd [here](https://github.com/ozonru/etcd/releases/tag/v3.3.20-grpc1.27-origmodule). Look at 445 | this [issue](https://github.com/etcd-io/etcd/issues/11721) to track -------------------------------------------------------------------------------- /denny.go: -------------------------------------------------------------------------------- 1 | package denny 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "google.golang.org/protobuf/encoding/protojson" 8 | "google.golang.org/protobuf/proto" 9 | "net" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "reflect" 14 | "regexp" 15 | "strings" 16 | "sync" 17 | "syscall" 18 | "time" 19 | 20 | "github.com/gin-gonic/gin" 21 | "github.com/gin-gonic/gin/binding" 22 | "github.com/golang/protobuf/ptypes/empty" 23 | "github.com/soheilhy/cmux" 24 | "github.com/whatvn/denny/log" 25 | "github.com/whatvn/denny/middleware" 26 | "github.com/whatvn/denny/naming" 27 | "google.golang.org/grpc" 28 | ) 29 | 30 | type ( 31 | Context = gin.Context 32 | HandleFunc = gin.HandlerFunc 33 | methodHandlerMap struct { 34 | method HttpMethod 35 | handler HandleFunc 36 | } 37 | group struct { 38 | path string 39 | cors bool 40 | routerGroup *gin.RouterGroup 41 | handlerMap map[string]*methodHandlerMap 42 | engine *Denny 43 | } 44 | 45 | Denny struct { 46 | sync.Mutex 47 | *log.Log 48 | handlerMap map[string]*methodHandlerMap 49 | groups []*group 50 | *gin.Engine 51 | initialised bool 52 | validator binding.StructValidator 53 | notFoundHandler HandleFunc 54 | noMethodHandler HandleFunc 55 | grpcServer *grpc.Server 56 | // for naming registry/dicovery 57 | registry naming.Registry 58 | } 59 | 60 | ProtoJsonSerializer func(response interface{}) (interface{}, error) 61 | ) 62 | 63 | var ( 64 | invalidMethodType = errors.New("invalid method signature") 65 | notFoundHandlerFunc = func(ctx *Context) { 66 | ctx.JSON( 67 | http.StatusOK, 68 | gin.H{ 69 | "path": ctx.Request.RequestURI, 70 | "status": http.StatusNotFound, 71 | "method": ctx.Request.Method, 72 | }, 73 | ) 74 | } 75 | 76 | underlyContextType = reflect.TypeOf(new(context.Context)).Elem() 77 | underlyErrorType = reflect.TypeOf(new(error)).Elem() 78 | 79 | brpcHTTPResponseParser ProtoJsonSerializer 80 | 81 | ProtoJsonResponseSerializer = func(m protojson.MarshalOptions) ProtoJsonSerializer { 82 | return func(response interface{}) (interface{}, error) { 83 | responseProto, ok := response.(proto.Message) 84 | if !ok { 85 | return response, nil 86 | } 87 | 88 | jsonBytes, err := m.Marshal(responseProto) 89 | if err != nil { 90 | return nil, err 91 | } 92 | var newResponse interface{} 93 | err = json.Unmarshal(jsonBytes, &newResponse) 94 | return newResponse, err 95 | } 96 | } 97 | ) 98 | 99 | const ( 100 | SelectCapital = "([a-z])([A-Z])" 101 | ReplaceCapital = "$1 $2" 102 | ) 103 | 104 | func newGroup(path string, routerGroup *gin.RouterGroup) *group { 105 | return &group{path: path, routerGroup: routerGroup} 106 | } 107 | 108 | // NewServer init denny with default parameter 109 | func NewServer(debug ...bool) *Denny { 110 | if len(debug) == 0 || !debug[0] { 111 | gin.SetMode(gin.ReleaseMode) 112 | } 113 | return &Denny{ 114 | handlerMap: make(map[string]*methodHandlerMap), 115 | groups: []*group{}, 116 | Engine: gin.New(), 117 | Log: log.New(), 118 | initialised: false, 119 | notFoundHandler: notFoundHandlerFunc, 120 | noMethodHandler: notFoundHandlerFunc, 121 | } 122 | } 123 | 124 | func AddProtoJsonResponseSerializer(parserFunc ProtoJsonSerializer) { 125 | brpcHTTPResponseParser = parserFunc 126 | } 127 | 128 | // WithRegistry makes Denny discoverable via naming registry 129 | func (r *Denny) WithRegistry(registry naming.Registry) *Denny { 130 | r.registry = registry 131 | return r 132 | } 133 | 134 | // WithGrpcServer turns Denny into grpc server 135 | func (r *Denny) WithGrpcServer(server *grpc.Server) *Denny { 136 | if server == nil { 137 | panic("server is not initialised") 138 | } 139 | r.grpcServer = server 140 | return r 141 | } 142 | 143 | // Controller register a controller with given path, method to http routes 144 | func (r *Denny) Controller(path string, method HttpMethod, ctl controller) *Denny { 145 | r.Lock() 146 | defer r.Unlock() 147 | if r.validator != nil { 148 | ctl.SetValidator(r.validator) 149 | } 150 | m := &methodHandlerMap{ 151 | method: method, 152 | handler: func(ctx *Context) { 153 | ctl.init() 154 | ctl.Handle(ctx) 155 | }, 156 | } 157 | 158 | r.handlerMap[path] = m 159 | return r 160 | } 161 | 162 | // NewGroup adds new group into server routes 163 | func (r *Denny) NewGroup(path string) *group { 164 | r.Lock() 165 | defer r.Unlock() 166 | routerGroup := r.Group(path) 167 | ng := newGroup(path, routerGroup) 168 | ng.engine = r 169 | r.groups = append(r.groups, ng) 170 | return ng 171 | } 172 | 173 | // same with WithMiddleware 174 | func (g *group) Use(handleFunc HandleFunc) *group { 175 | g.routerGroup.Use(handleFunc) 176 | return g 177 | } 178 | 179 | // Controller is the same with router Controller, but register a controller with given path within group 180 | func (g *group) Controller(path string, method HttpMethod, ctl controller) *group { 181 | if g.engine.validator != nil { 182 | ctl.SetValidator(g.engine.validator) 183 | } 184 | m := &methodHandlerMap{ 185 | method: method, 186 | handler: func(ctx *Context) { 187 | ctl.init() 188 | ctl.Handle(ctx) 189 | }, 190 | } 191 | if g.handlerMap == nil { 192 | g.handlerMap = make(map[string]*methodHandlerMap) 193 | } 194 | 195 | g.handlerMap[path] = m 196 | return g 197 | } 198 | 199 | func toKebabCase(input string, rule ...string) string { 200 | 201 | re := regexp.MustCompile(SelectCapital) 202 | input = re.ReplaceAllString(input, ReplaceCapital) 203 | 204 | input = strings.Join(strings.Fields(strings.TrimSpace(input)), " ") 205 | 206 | rule = append(rule, ".", " ", "_", " ", "-", " ") 207 | 208 | replacer := strings.NewReplacer(rule...) 209 | input = replacer.Replace(input) 210 | return strings.ToLower(strings.Join(strings.Fields(input), "-")) 211 | } 212 | 213 | func httpMethod(method reflect.Method) HttpMethod { 214 | in := method.Type.In(2) 215 | if in == reflect.TypeOf(&empty.Empty{}) { 216 | return HttpGet 217 | } 218 | return HttpPost 219 | } 220 | 221 | func httpRouterPath(controllerName string, method reflect.Method) string { 222 | return toKebabCase(controllerName) + "/" + toKebabCase(method.Name) 223 | } 224 | 225 | // BrpcController register a grpc service implements as multiple http enpoints 226 | // endpoint usually start with service name (class name), and end with method name 227 | // so if your have your service name: Greeting and have method hi, when registered with server 228 | // under group v1, your http endpoint will be /v1/greeting/hi 229 | func (g *group) BrpcController(controllerGroup interface{}) { 230 | g.registerHttpController(controllerGroup) 231 | } 232 | 233 | func (g *group) WithCors() { 234 | g.cors = true 235 | } 236 | 237 | func cors() HandleFunc { 238 | return func(c *gin.Context) { 239 | c.Writer.Header().Set("Access-Control-Allow-Origin", c.Request.Header.Get("Origin")) 240 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 241 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Access-Control-Allow-Origin, Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") 242 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") 243 | 244 | if c.Request.Method == "OPTIONS" { 245 | c.AbortWithStatus(204) 246 | return 247 | } 248 | 249 | c.Next() 250 | } 251 | } 252 | 253 | func (g *group) registerHttpController(controllerGroup interface{}) { 254 | var ( 255 | controllerReferenceType = reflect.TypeOf(controllerGroup) 256 | controllerReferenceValue = reflect.ValueOf(controllerGroup) 257 | indirectControllerReferenceValueType = reflect.Indirect(controllerReferenceValue).Type() 258 | controllerName = indirectControllerReferenceValueType.Name() 259 | ) 260 | // Install the methods 261 | for m := 0; m < controllerReferenceType.NumMethod(); m++ { 262 | 263 | method := controllerReferenceType.Method(m) 264 | 265 | if method.Type.NumIn() != 3 { 266 | panic(invalidMethodType) 267 | } 268 | 269 | groupType, contextType, requestType := method.Type.In(0), method.Type.In(1), method.Type.In(2) 270 | 271 | if groupType.Kind() != reflect.Ptr { 272 | panic(invalidMethodType) 273 | } 274 | 275 | if contextType.Kind() != reflect.Ptr && contextType.Kind() != reflect.Interface { 276 | panic(invalidMethodType) 277 | } 278 | 279 | if !contextType.Implements(underlyContextType) { 280 | panic(invalidMethodType) 281 | } 282 | 283 | if requestType.Kind() != reflect.Ptr { 284 | panic(invalidMethodType) 285 | } 286 | 287 | if method.Type.NumOut() != 2 { 288 | panic(invalidMethodType) 289 | } 290 | 291 | outErrorType, outResponseType := method.Type.Out(1), method.Type.Out(0) 292 | if outErrorType != underlyErrorType { 293 | panic(invalidMethodType) 294 | } 295 | 296 | if outResponseType.Kind() != reflect.Ptr { 297 | panic(invalidMethodType) 298 | } 299 | 300 | routerPath, httpMethod := httpRouterPath(controllerName, method), httpMethod(method) 301 | g.registerHandler(controllerReferenceValue, method, routerPath, httpMethod) 302 | 303 | } 304 | } 305 | 306 | func unmarshal(ctx *Context, in interface{}) error { 307 | return ctx.ShouldBind(in) 308 | } 309 | 310 | // getCaller extract grpc service implementation into gin http handlerFunc 311 | func getCaller(fn, obj reflect.Value) (func(*gin.Context), error) { 312 | var ( 313 | funcType = fn.Type() 314 | requestType = funcType.In(2) 315 | ) 316 | 317 | handlerFunc := func(c *gin.Context) interface{} { return c } 318 | 319 | reqIsValue := true 320 | if requestType.Kind() == reflect.Ptr { 321 | reqIsValue = false 322 | } 323 | return func(c *Context) { 324 | req := reflect.New(requestType) 325 | if !reqIsValue { 326 | req = reflect.New(requestType.Elem()) 327 | } 328 | if err := unmarshal(c, req.Interface()); err != nil { 329 | c.JSON(http.StatusBadRequest, err) 330 | return 331 | } 332 | 333 | if reqIsValue { 334 | req = req.Elem() 335 | } 336 | 337 | // check if request has validate function enabled 338 | if v, ok := req.Interface().(middleware.IValidator); ok { 339 | if err := v.Validate(); err != nil { 340 | _ = c.AbortWithError(http.StatusBadRequest, err) 341 | return 342 | } 343 | } 344 | 345 | var vals []reflect.Value 346 | // call grpc service with provided method 347 | // obj is service which implements grpc service interface 348 | vals = fn.Call([]reflect.Value{obj, reflect.ValueOf(handlerFunc(c)), req}) 349 | 350 | if vals != nil { 351 | response, err := vals[0].Interface(), vals[1].Interface() 352 | if err != nil { 353 | _ = c.AbortWithError(http.StatusInternalServerError, err.(error)) 354 | return 355 | } 356 | 357 | if brpcHTTPResponseParser != nil { 358 | res, err := brpcHTTPResponseParser(response) 359 | if err != nil { 360 | _ = c.AbortWithError(http.StatusInternalServerError, err) 361 | return 362 | } 363 | c.JSON(http.StatusOK, res) 364 | return 365 | } 366 | 367 | c.JSON(http.StatusOK, response) 368 | return 369 | } 370 | c.JSON(http.StatusInternalServerError, gin.H{}) 371 | }, nil 372 | } 373 | 374 | func handlerFuncObj(function, obj reflect.Value) gin.HandlerFunc { 375 | call, err := getCaller(function, obj) 376 | if err != nil { 377 | panic(err) 378 | } 379 | 380 | return call 381 | } 382 | 383 | func (g *group) registerHandler( 384 | controllerReferenceValue reflect.Value, 385 | method reflect.Method, path string, httpMethod HttpMethod) { 386 | handlerFunc := handlerFuncObj(method.Func, controllerReferenceValue) 387 | if g.cors { 388 | g.routerGroup.OPTIONS(path, cors()) 389 | } 390 | switch httpMethod { 391 | case HttpPost: 392 | g.routerGroup.POST(path, cors(), handlerFunc) 393 | break 394 | case HttpGet: 395 | g.routerGroup.GET(path, cors(), handlerFunc) 396 | break 397 | default: 398 | panic("not implemetation") 399 | } 400 | } 401 | 402 | func (r *Denny) initRoute() { 403 | if r.initialised { 404 | return 405 | } 406 | for p, m := range r.handlerMap { 407 | setupHandler(m, r, p) 408 | } 409 | 410 | for _, g := range r.groups { 411 | for p, m := range g.handlerMap { 412 | setupHandler(m, g.routerGroup, p) 413 | } 414 | } 415 | r.RemoveExtraSlash = true 416 | r.NoRoute(r.notFoundHandler) 417 | r.NoMethod(r.noMethodHandler) 418 | r.WithMiddleware(gin.Recovery()) 419 | r.initialised = true 420 | } 421 | 422 | func setupHandler(m *methodHandlerMap, router gin.IRouter, p string) { 423 | switch m.method { 424 | case HttpGet: 425 | router.GET(p, m.handler) 426 | case HttpPost: 427 | router.POST(p, m.handler) 428 | case HttpDelete: 429 | router.DELETE(p, m.handler) 430 | case HttpOption: 431 | router.OPTIONS(p, m.handler) 432 | case HttpPatch: 433 | router.PATCH(p, m.handler) 434 | } 435 | } 436 | 437 | // ServeHTTP conforms to the http.Handler interface. 438 | func (r *Denny) ServeHTTP(w http.ResponseWriter, req *http.Request) { 439 | r.initRoute() 440 | r.Engine.ServeHTTP(w, req) 441 | } 442 | 443 | // WithMiddleware registers middleware to http server 444 | // only gin style middleware is supported 445 | func (r *Denny) WithMiddleware(middleware ...HandleFunc) { 446 | r.Use(middleware...) 447 | } 448 | 449 | // WithNotFoundHandler registers middleware to http server to serve not found route 450 | // only gin style middleware is supported 451 | func (r *Denny) WithNotFoundHandler(handler HandleFunc) { 452 | r.notFoundHandler = handler 453 | } 454 | 455 | // Start http server with given address 456 | // Deprecated: use GraceFulStart(addrs ...string) instead. 457 | func (r *Denny) Start(addrs ...string) error { 458 | r.initRoute() 459 | return r.Run(addrs...) 460 | } 461 | 462 | // SetValidator overwrites default gin validate with provides validator 463 | // we're using v10 validator 464 | func (r *Denny) SetValidator(v binding.StructValidator) *Denny { 465 | r.validator = v 466 | return r 467 | } 468 | 469 | // GraceFulStart uses net http standard server 470 | // it also detect if grpc server and discovery registry are available 471 | // to start Denny in brpc mode, in this mode, server will support both protocol using same port 472 | // and register channel listen to os signals to make it graceful stop 473 | func (r *Denny) GraceFulStart(addrs ...string) error { 474 | var ( 475 | httpSrv = &http.Server{ 476 | Handler: r, 477 | } 478 | enableBrpc = r.grpcServer != nil 479 | listener net.Listener 480 | grpcListener net.Listener 481 | httpListener net.Listener 482 | muxer cmux.CMux 483 | err error 484 | addr = r.resolveAddress(addrs) 485 | ip string 486 | ) 487 | 488 | r.initRoute() 489 | enableHttp := len(r.Handlers) > 0 || len(r.groups) > 0 490 | 491 | if enableBrpc { 492 | listener, err = net.Listen("tcp", addr) 493 | if err != nil { 494 | r.Fatalf("listen: %v\n", err) 495 | } 496 | 497 | // register service into registered registry 498 | if r.registry != nil { 499 | ip, err = localIp() 500 | if err != nil { 501 | panic(err) 502 | } 503 | if err = r.registry.Register(ip+addr, 5); err != nil { 504 | panic(err) 505 | } 506 | } 507 | 508 | // Create a cmux. 509 | muxer = cmux.New(listener) 510 | // Match connections in order: 511 | // First grpc, then HTTP, and otherwise Go RPC/TCP. 512 | grpcListener = muxer.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) 513 | httpListener = muxer.Match(cmux.HTTP1Fast()) 514 | 515 | wg := sync.WaitGroup{} 516 | if enableHttp { 517 | wg.Add(1) 518 | } 519 | if enableBrpc { 520 | wg.Add(1) 521 | } 522 | 523 | go func() { 524 | r.Info("start grpc server ", addr) 525 | wg.Done() 526 | if err := r.grpcServer.Serve(grpcListener); err != nil { 527 | r.Fatalf("listen: %v\n", err) 528 | } 529 | }() 530 | 531 | if enableHttp { 532 | go func() { 533 | r.Info("start http server ", addr) 534 | wg.Done() 535 | if err := httpSrv.Serve(httpListener); err != nil && err != http.ErrServerClosed { 536 | r.Fatalf("listen: %v\n", err) 537 | } 538 | }() 539 | } 540 | 541 | go func() { 542 | if err = muxer.Serve(); err != nil { 543 | r.Fatalf("listen: %v\n", err) 544 | } 545 | }() 546 | 547 | } else { 548 | go func() { 549 | // service connections 550 | httpSrv.Addr = addr 551 | r.Info("start http server ", addr) 552 | if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 553 | r.Fatalf("listen: %v\n", err) 554 | } 555 | }() 556 | } 557 | 558 | // Wait for interrupt signal to gracefully shutdown the server with 559 | // a timeout of 5 seconds. 560 | quit := make(chan os.Signal, 1) 561 | signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT) 562 | <-quit 563 | 564 | r.Infof("Shutdown Server ...") 565 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 566 | defer cancel() 567 | 568 | if r.registry != nil { 569 | r.Infof("unregister from registry") 570 | _ = r.registry.UnRegister(ip + addr) 571 | } 572 | if r.grpcServer != nil { 573 | r.Infof("stop grpc server") 574 | r.grpcServer.GracefulStop() 575 | } 576 | 577 | if enableHttp { 578 | r.Infof("stop http server") 579 | _ = httpSrv.Shutdown(ctx) 580 | } 581 | 582 | return nil 583 | } 584 | 585 | func (r *Denny) resolveAddress(addr []string) string { 586 | switch len(addr) { 587 | case 0: 588 | if port := os.Getenv("PORT"); port != "" { 589 | r.Debugf("environment variable PORT=\"%s\"", port) 590 | return ":" + port 591 | } 592 | r.Debug("environment variable PORT is undefined. Using port :8080 by default") 593 | return ":8080" 594 | case 1: 595 | return addr[0] 596 | default: 597 | panic("too many parameters") 598 | } 599 | } 600 | 601 | func localIp() (string, error) { 602 | addrs, err := net.InterfaceAddrs() 603 | if err != nil { 604 | return "", err 605 | } 606 | 607 | for _, a := range addrs { 608 | if ipnet, ok := a.(*net.IPNet); ok && 609 | !ipnet.IP.IsLoopback() { 610 | ipv4 := ipnet.IP.To4() 611 | if ipv4 != nil && strings.Index(ipv4.String(), "127") != 0 { 612 | return ipv4.String(), nil 613 | } 614 | } 615 | } 616 | return "", errors.New("cannot lookup local ip address") 617 | } 618 | -------------------------------------------------------------------------------- /example/protobuf/hello.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.26.0 4 | // protoc v3.15.6 5 | // source: protobuf/hello.proto 6 | 7 | package protobuf 8 | 9 | import ( 10 | context "context" 11 | _ "github.com/envoyproxy/protoc-gen-validate/validate" 12 | grpc "google.golang.org/grpc" 13 | codes "google.golang.org/grpc/codes" 14 | status "google.golang.org/grpc/status" 15 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 16 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 17 | emptypb "google.golang.org/protobuf/types/known/emptypb" 18 | timestamppb "google.golang.org/protobuf/types/known/timestamppb" 19 | reflect "reflect" 20 | sync "sync" 21 | ) 22 | 23 | const ( 24 | // Verify that this generated code is sufficiently up-to-date. 25 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 26 | // Verify that runtime/protoimpl is sufficiently up-to-date. 27 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 28 | ) 29 | 30 | type Status int32 31 | 32 | const ( 33 | Status_STATUS_SUCCESS Status = 0 34 | Status_STATUS_FAIL Status = 1 35 | ) 36 | 37 | // Enum value maps for Status. 38 | var ( 39 | Status_name = map[int32]string{ 40 | 0: "STATUS_SUCCESS", 41 | 1: "STATUS_FAIL", 42 | } 43 | Status_value = map[string]int32{ 44 | "STATUS_SUCCESS": 0, 45 | "STATUS_FAIL": 1, 46 | } 47 | ) 48 | 49 | func (x Status) Enum() *Status { 50 | p := new(Status) 51 | *p = x 52 | return p 53 | } 54 | 55 | func (x Status) String() string { 56 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 57 | } 58 | 59 | func (Status) Descriptor() protoreflect.EnumDescriptor { 60 | return file_protobuf_hello_proto_enumTypes[0].Descriptor() 61 | } 62 | 63 | func (Status) Type() protoreflect.EnumType { 64 | return &file_protobuf_hello_proto_enumTypes[0] 65 | } 66 | 67 | func (x Status) Number() protoreflect.EnumNumber { 68 | return protoreflect.EnumNumber(x) 69 | } 70 | 71 | // Deprecated: Use Status.Descriptor instead. 72 | func (Status) EnumDescriptor() ([]byte, []int) { 73 | return file_protobuf_hello_proto_rawDescGZIP(), []int{0} 74 | } 75 | 76 | type HelloRequest struct { 77 | state protoimpl.MessageState 78 | sizeCache protoimpl.SizeCache 79 | unknownFields protoimpl.UnknownFields 80 | 81 | Greeting string `protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"` 82 | } 83 | 84 | func (x *HelloRequest) Reset() { 85 | *x = HelloRequest{} 86 | if protoimpl.UnsafeEnabled { 87 | mi := &file_protobuf_hello_proto_msgTypes[0] 88 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 89 | ms.StoreMessageInfo(mi) 90 | } 91 | } 92 | 93 | func (x *HelloRequest) String() string { 94 | return protoimpl.X.MessageStringOf(x) 95 | } 96 | 97 | func (*HelloRequest) ProtoMessage() {} 98 | 99 | func (x *HelloRequest) ProtoReflect() protoreflect.Message { 100 | mi := &file_protobuf_hello_proto_msgTypes[0] 101 | if protoimpl.UnsafeEnabled && x != nil { 102 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 103 | if ms.LoadMessageInfo() == nil { 104 | ms.StoreMessageInfo(mi) 105 | } 106 | return ms 107 | } 108 | return mi.MessageOf(x) 109 | } 110 | 111 | // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. 112 | func (*HelloRequest) Descriptor() ([]byte, []int) { 113 | return file_protobuf_hello_proto_rawDescGZIP(), []int{0} 114 | } 115 | 116 | func (x *HelloRequest) GetGreeting() string { 117 | if x != nil { 118 | return x.Greeting 119 | } 120 | return "" 121 | } 122 | 123 | type HelloResponse struct { 124 | state protoimpl.MessageState 125 | sizeCache protoimpl.SizeCache 126 | unknownFields protoimpl.UnknownFields 127 | 128 | Reply string `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"` 129 | CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` 130 | } 131 | 132 | func (x *HelloResponse) Reset() { 133 | *x = HelloResponse{} 134 | if protoimpl.UnsafeEnabled { 135 | mi := &file_protobuf_hello_proto_msgTypes[1] 136 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 137 | ms.StoreMessageInfo(mi) 138 | } 139 | } 140 | 141 | func (x *HelloResponse) String() string { 142 | return protoimpl.X.MessageStringOf(x) 143 | } 144 | 145 | func (*HelloResponse) ProtoMessage() {} 146 | 147 | func (x *HelloResponse) ProtoReflect() protoreflect.Message { 148 | mi := &file_protobuf_hello_proto_msgTypes[1] 149 | if protoimpl.UnsafeEnabled && x != nil { 150 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 151 | if ms.LoadMessageInfo() == nil { 152 | ms.StoreMessageInfo(mi) 153 | } 154 | return ms 155 | } 156 | return mi.MessageOf(x) 157 | } 158 | 159 | // Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead. 160 | func (*HelloResponse) Descriptor() ([]byte, []int) { 161 | return file_protobuf_hello_proto_rawDescGZIP(), []int{1} 162 | } 163 | 164 | func (x *HelloResponse) GetReply() string { 165 | if x != nil { 166 | return x.Reply 167 | } 168 | return "" 169 | } 170 | 171 | func (x *HelloResponse) GetCreatedAt() *timestamppb.Timestamp { 172 | if x != nil { 173 | return x.CreatedAt 174 | } 175 | return nil 176 | } 177 | 178 | type HelloResponseAnonymous struct { 179 | state protoimpl.MessageState 180 | sizeCache protoimpl.SizeCache 181 | unknownFields protoimpl.UnknownFields 182 | 183 | Reply string `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"` 184 | Status Status `protobuf:"varint,2,opt,name=status,proto3,enum=pb.Status" json:"status,omitempty"` 185 | CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` 186 | } 187 | 188 | func (x *HelloResponseAnonymous) Reset() { 189 | *x = HelloResponseAnonymous{} 190 | if protoimpl.UnsafeEnabled { 191 | mi := &file_protobuf_hello_proto_msgTypes[2] 192 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 193 | ms.StoreMessageInfo(mi) 194 | } 195 | } 196 | 197 | func (x *HelloResponseAnonymous) String() string { 198 | return protoimpl.X.MessageStringOf(x) 199 | } 200 | 201 | func (*HelloResponseAnonymous) ProtoMessage() {} 202 | 203 | func (x *HelloResponseAnonymous) ProtoReflect() protoreflect.Message { 204 | mi := &file_protobuf_hello_proto_msgTypes[2] 205 | if protoimpl.UnsafeEnabled && x != nil { 206 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 207 | if ms.LoadMessageInfo() == nil { 208 | ms.StoreMessageInfo(mi) 209 | } 210 | return ms 211 | } 212 | return mi.MessageOf(x) 213 | } 214 | 215 | // Deprecated: Use HelloResponseAnonymous.ProtoReflect.Descriptor instead. 216 | func (*HelloResponseAnonymous) Descriptor() ([]byte, []int) { 217 | return file_protobuf_hello_proto_rawDescGZIP(), []int{2} 218 | } 219 | 220 | func (x *HelloResponseAnonymous) GetReply() string { 221 | if x != nil { 222 | return x.Reply 223 | } 224 | return "" 225 | } 226 | 227 | func (x *HelloResponseAnonymous) GetStatus() Status { 228 | if x != nil { 229 | return x.Status 230 | } 231 | return Status_STATUS_SUCCESS 232 | } 233 | 234 | func (x *HelloResponseAnonymous) GetCreatedAt() *timestamppb.Timestamp { 235 | if x != nil { 236 | return x.CreatedAt 237 | } 238 | return nil 239 | } 240 | 241 | var File_protobuf_hello_proto protoreflect.FileDescriptor 242 | 243 | var file_protobuf_hello_proto_rawDesc = []byte{ 244 | 0x0a, 0x14, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 245 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x70, 0x62, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 246 | 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 247 | 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 248 | 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 249 | 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 250 | 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 251 | 0x6f, 0x22, 0x33, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 252 | 0x74, 0x12, 0x23, 0x0a, 0x08, 0x67, 0x72, 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 253 | 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x08, 0x67, 0x72, 254 | 0x65, 0x65, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x60, 0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 255 | 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 256 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x39, 0x0a, 257 | 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 258 | 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 259 | 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 260 | 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x8d, 0x01, 0x0a, 0x16, 0x48, 0x65, 0x6c, 261 | 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x41, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 262 | 0x6f, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 263 | 0x28, 0x09, 0x52, 0x05, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x22, 0x0a, 0x06, 0x73, 0x74, 0x61, 264 | 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x2e, 0x53, 265 | 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x39, 0x0a, 266 | 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 267 | 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 268 | 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 269 | 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x2a, 0x2d, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 270 | 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 271 | 0x43, 0x45, 0x53, 0x53, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 272 | 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x10, 0x01, 0x32, 0x88, 0x01, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 273 | 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 274 | 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x10, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 275 | 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 276 | 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x11, 0x53, 0x61, 0x79, 277 | 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x41, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 0x75, 0x73, 0x12, 0x16, 278 | 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 279 | 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x70, 0x62, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 280 | 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x41, 0x6e, 0x6f, 0x6e, 0x79, 0x6d, 0x6f, 281 | 0x75, 0x73, 0x42, 0x12, 0x5a, 0x10, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x70, 0x72, 282 | 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 283 | } 284 | 285 | var ( 286 | file_protobuf_hello_proto_rawDescOnce sync.Once 287 | file_protobuf_hello_proto_rawDescData = file_protobuf_hello_proto_rawDesc 288 | ) 289 | 290 | func file_protobuf_hello_proto_rawDescGZIP() []byte { 291 | file_protobuf_hello_proto_rawDescOnce.Do(func() { 292 | file_protobuf_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_protobuf_hello_proto_rawDescData) 293 | }) 294 | return file_protobuf_hello_proto_rawDescData 295 | } 296 | 297 | var file_protobuf_hello_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 298 | var file_protobuf_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 299 | var file_protobuf_hello_proto_goTypes = []interface{}{ 300 | (Status)(0), // 0: pb.Status 301 | (*HelloRequest)(nil), // 1: pb.HelloRequest 302 | (*HelloResponse)(nil), // 2: pb.HelloResponse 303 | (*HelloResponseAnonymous)(nil), // 3: pb.HelloResponseAnonymous 304 | (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp 305 | (*emptypb.Empty)(nil), // 5: google.protobuf.Empty 306 | } 307 | var file_protobuf_hello_proto_depIdxs = []int32{ 308 | 4, // 0: pb.HelloResponse.created_at:type_name -> google.protobuf.Timestamp 309 | 0, // 1: pb.HelloResponseAnonymous.status:type_name -> pb.Status 310 | 4, // 2: pb.HelloResponseAnonymous.created_at:type_name -> google.protobuf.Timestamp 311 | 1, // 3: pb.HelloService.SayHello:input_type -> pb.HelloRequest 312 | 5, // 4: pb.HelloService.SayHelloAnonymous:input_type -> google.protobuf.Empty 313 | 2, // 5: pb.HelloService.SayHello:output_type -> pb.HelloResponse 314 | 3, // 6: pb.HelloService.SayHelloAnonymous:output_type -> pb.HelloResponseAnonymous 315 | 5, // [5:7] is the sub-list for method output_type 316 | 3, // [3:5] is the sub-list for method input_type 317 | 3, // [3:3] is the sub-list for extension type_name 318 | 3, // [3:3] is the sub-list for extension extendee 319 | 0, // [0:3] is the sub-list for field type_name 320 | } 321 | 322 | func init() { file_protobuf_hello_proto_init() } 323 | func file_protobuf_hello_proto_init() { 324 | if File_protobuf_hello_proto != nil { 325 | return 326 | } 327 | if !protoimpl.UnsafeEnabled { 328 | file_protobuf_hello_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 329 | switch v := v.(*HelloRequest); i { 330 | case 0: 331 | return &v.state 332 | case 1: 333 | return &v.sizeCache 334 | case 2: 335 | return &v.unknownFields 336 | default: 337 | return nil 338 | } 339 | } 340 | file_protobuf_hello_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 341 | switch v := v.(*HelloResponse); i { 342 | case 0: 343 | return &v.state 344 | case 1: 345 | return &v.sizeCache 346 | case 2: 347 | return &v.unknownFields 348 | default: 349 | return nil 350 | } 351 | } 352 | file_protobuf_hello_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 353 | switch v := v.(*HelloResponseAnonymous); i { 354 | case 0: 355 | return &v.state 356 | case 1: 357 | return &v.sizeCache 358 | case 2: 359 | return &v.unknownFields 360 | default: 361 | return nil 362 | } 363 | } 364 | } 365 | type x struct{} 366 | out := protoimpl.TypeBuilder{ 367 | File: protoimpl.DescBuilder{ 368 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 369 | RawDescriptor: file_protobuf_hello_proto_rawDesc, 370 | NumEnums: 1, 371 | NumMessages: 3, 372 | NumExtensions: 0, 373 | NumServices: 1, 374 | }, 375 | GoTypes: file_protobuf_hello_proto_goTypes, 376 | DependencyIndexes: file_protobuf_hello_proto_depIdxs, 377 | EnumInfos: file_protobuf_hello_proto_enumTypes, 378 | MessageInfos: file_protobuf_hello_proto_msgTypes, 379 | }.Build() 380 | File_protobuf_hello_proto = out.File 381 | file_protobuf_hello_proto_rawDesc = nil 382 | file_protobuf_hello_proto_goTypes = nil 383 | file_protobuf_hello_proto_depIdxs = nil 384 | } 385 | 386 | // Reference imports to suppress errors if they are not otherwise used. 387 | var _ context.Context 388 | var _ grpc.ClientConnInterface 389 | 390 | // This is a compile-time assertion to ensure that this generated file 391 | // is compatible with the grpc package it is being compiled against. 392 | const _ = grpc.SupportPackageIsVersion6 393 | 394 | // HelloServiceClient is the client API for HelloService service. 395 | // 396 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 397 | type HelloServiceClient interface { 398 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) 399 | SayHelloAnonymous(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HelloResponseAnonymous, error) 400 | } 401 | 402 | type helloServiceClient struct { 403 | cc grpc.ClientConnInterface 404 | } 405 | 406 | func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient { 407 | return &helloServiceClient{cc} 408 | } 409 | 410 | func (c *helloServiceClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) { 411 | out := new(HelloResponse) 412 | err := c.cc.Invoke(ctx, "/pb.HelloService/SayHello", in, out, opts...) 413 | if err != nil { 414 | return nil, err 415 | } 416 | return out, nil 417 | } 418 | 419 | func (c *helloServiceClient) SayHelloAnonymous(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HelloResponseAnonymous, error) { 420 | out := new(HelloResponseAnonymous) 421 | err := c.cc.Invoke(ctx, "/pb.HelloService/SayHelloAnonymous", in, out, opts...) 422 | if err != nil { 423 | return nil, err 424 | } 425 | return out, nil 426 | } 427 | 428 | // HelloServiceServer is the server API for HelloService service. 429 | type HelloServiceServer interface { 430 | SayHello(context.Context, *HelloRequest) (*HelloResponse, error) 431 | SayHelloAnonymous(context.Context, *emptypb.Empty) (*HelloResponseAnonymous, error) 432 | } 433 | 434 | // UnimplementedHelloServiceServer can be embedded to have forward compatible implementations. 435 | type UnimplementedHelloServiceServer struct { 436 | } 437 | 438 | func (*UnimplementedHelloServiceServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) { 439 | return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") 440 | } 441 | func (*UnimplementedHelloServiceServer) SayHelloAnonymous(context.Context, *emptypb.Empty) (*HelloResponseAnonymous, error) { 442 | return nil, status.Errorf(codes.Unimplemented, "method SayHelloAnonymous not implemented") 443 | } 444 | 445 | func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) { 446 | s.RegisterService(&_HelloService_serviceDesc, srv) 447 | } 448 | 449 | func _HelloService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 450 | in := new(HelloRequest) 451 | if err := dec(in); err != nil { 452 | return nil, err 453 | } 454 | if interceptor == nil { 455 | return srv.(HelloServiceServer).SayHello(ctx, in) 456 | } 457 | info := &grpc.UnaryServerInfo{ 458 | Server: srv, 459 | FullMethod: "/pb.HelloService/SayHello", 460 | } 461 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 462 | return srv.(HelloServiceServer).SayHello(ctx, req.(*HelloRequest)) 463 | } 464 | return interceptor(ctx, in, info, handler) 465 | } 466 | 467 | func _HelloService_SayHelloAnonymous_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 468 | in := new(emptypb.Empty) 469 | if err := dec(in); err != nil { 470 | return nil, err 471 | } 472 | if interceptor == nil { 473 | return srv.(HelloServiceServer).SayHelloAnonymous(ctx, in) 474 | } 475 | info := &grpc.UnaryServerInfo{ 476 | Server: srv, 477 | FullMethod: "/pb.HelloService/SayHelloAnonymous", 478 | } 479 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 480 | return srv.(HelloServiceServer).SayHelloAnonymous(ctx, req.(*emptypb.Empty)) 481 | } 482 | return interceptor(ctx, in, info, handler) 483 | } 484 | 485 | var _HelloService_serviceDesc = grpc.ServiceDesc{ 486 | ServiceName: "pb.HelloService", 487 | HandlerType: (*HelloServiceServer)(nil), 488 | Methods: []grpc.MethodDesc{ 489 | { 490 | MethodName: "SayHello", 491 | Handler: _HelloService_SayHello_Handler, 492 | }, 493 | { 494 | MethodName: "SayHelloAnonymous", 495 | Handler: _HelloService_SayHelloAnonymous_Handler, 496 | }, 497 | }, 498 | Streams: []grpc.StreamDesc{}, 499 | Metadata: "protobuf/hello.proto", 500 | } 501 | --------------------------------------------------------------------------------