diff --git a/client/client.go b/client/client.go index da13c8e..4a65930 100644 --- a/client/client.go +++ b/client/client.go @@ -1 +1,284 @@ package client + +import ( + "git.noahlan.cn/northlan/ngs/internal/codec" + "git.noahlan.cn/northlan/ngs/internal/log" + "git.noahlan.cn/northlan/ngs/internal/message" + "git.noahlan.cn/northlan/ngs/internal/packet" + "google.golang.org/protobuf/proto" + "net" + "sync" +) + +var ( + hsd []byte // handshake data + had []byte // handshake ack data +) + +func init() { + var err error + hsd, err = codec.Encode(packet.Handshake, nil) + if err != nil { + panic(err) + } + + had, err = codec.Encode(packet.HandshakeAck, nil) + if err != nil { + panic(err) + } +} + +type ( + // Callback represents the callback type which will be called + // when the correspond events is occurred. + Callback func(data interface{}) + + // Client is a tiny Ngs client + Client struct { + conn net.Conn // low-level connection + codec *codec.Decoder // decoder + die chan struct{} // connector close channel + chSend chan []byte // send queue + mid uint64 // message id + + // events handler + muEvents sync.RWMutex + events map[string]Callback + + // response handler + muResponses sync.RWMutex + responses map[uint64]Callback + + connectedCallback func() // connected callback + } +) + +// NewClient create a new Client +func NewClient() *Client { + return &Client{ + die: make(chan struct{}), + codec: codec.NewDecoder(), + chSend: make(chan []byte, 64), + mid: 1, + events: map[string]Callback{}, + responses: map[uint64]Callback{}, + } +} + +// Start connect to the server and send/recv between the c/s +func (c *Client) Start(addr string) error { + conn, err := net.Dial("tcp", addr) + if err != nil { + return err + } + + c.conn = conn + + go c.write() + + // send handshake packet + c.send(hsd) + + // read and process network message + go c.read() + + return nil +} + +// OnConnected set the callback which will be called when the client connected to the server +func (c *Client) OnConnected(callback func()) { + c.connectedCallback = callback +} + +// Request send a request to server and register a callback for the response +func (c *Client) Request(route string, v proto.Message, callback Callback) error { + data, err := serialize(v) + if err != nil { + return err + } + + msg := &message.Message{ + Type: message.Request, + Route: route, + ID: c.mid, + Data: data, + } + + c.setResponseHandler(c.mid, callback) + if err := c.sendMessage(msg); err != nil { + c.setResponseHandler(c.mid, nil) + return err + } + + return nil +} + +// Notify send a notification to server +func (c *Client) Notify(route string, v proto.Message) error { + data, err := serialize(v) + if err != nil { + return err + } + + msg := &message.Message{ + Type: message.Notify, + Route: route, + Data: data, + } + return c.sendMessage(msg) +} + +// On add the callback for the event +func (c *Client) On(event string, callback Callback) { + c.muEvents.Lock() + defer c.muEvents.Unlock() + + c.events[event] = callback +} + +// Close the connection, and shutdown the benchmark +func (c *Client) Close() { + c.conn.Close() + close(c.die) +} + +func (c *Client) eventHandler(event string) (Callback, bool) { + c.muEvents.RLock() + defer c.muEvents.RUnlock() + + cb, ok := c.events[event] + return cb, ok +} + +func (c *Client) responseHandler(mid uint64) (Callback, bool) { + c.muResponses.RLock() + defer c.muResponses.RUnlock() + + cb, ok := c.responses[mid] + return cb, ok +} + +func (c *Client) setResponseHandler(mid uint64, cb Callback) { + c.muResponses.Lock() + defer c.muResponses.Unlock() + + if cb == nil { + delete(c.responses, mid) + } else { + c.responses[mid] = cb + } +} + +func (c *Client) sendMessage(msg *message.Message) error { + data, err := msg.Encode() + if err != nil { + return err + } + + //log.Printf("%+v",msg) + + payload, err := codec.Encode(packet.Data, data) + if err != nil { + return err + } + + c.mid++ + c.send(payload) + + return nil +} + +func (c *Client) write() { + defer close(c.chSend) + + for { + select { + case data := <-c.chSend: + if _, err := c.conn.Write(data); err != nil { + log.Println(err.Error()) + c.Close() + } + + case <-c.die: + return + } + } +} + +func (c *Client) send(data []byte) { + c.chSend <- data +} + +func (c *Client) read() { + buf := make([]byte, 2048) + + for { + n, err := c.conn.Read(buf) + if err != nil { + log.Println(err.Error()) + c.Close() + return + } + + packets, err := c.codec.Decode(buf[:n]) + if err != nil { + log.Println(err.Error()) + c.Close() + return + } + + for i := range packets { + p := packets[i] + c.processPacket(p) + } + } +} + +func (c *Client) processPacket(p *packet.Packet) { + switch p.Type { + case packet.Handshake: + c.send(had) + c.connectedCallback() + case packet.Data: + msg, err := message.Decode(p.Data) + if err != nil { + log.Println(err.Error()) + return + } + c.processMessage(msg) + + case packet.Kick: + c.Close() + } +} + +func (c *Client) processMessage(msg *message.Message) { + switch msg.Type { + case message.Push: + cb, ok := c.eventHandler(msg.Route) + if !ok { + log.Println("event handler not found", msg.Route) + return + } + + cb(msg.Data) + + case message.Response: + cb, ok := c.responseHandler(msg.ID) + if !ok { + log.Println("response handler not found", msg.ID) + return + } + + cb(msg.Data) + c.setResponseHandler(msg.ID, nil) + } +} + +func serialize(v proto.Message) ([]byte, error) { + data, err := proto.Marshal(v) + if err != nil { + return nil, err + } + return data, nil +} diff --git a/client/client_test.go b/client/client_test.go index da13c8e..620dbe1 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1 +1,102 @@ package client + +import ( + "git.noahlan.cn/northlan/ngs" + "git.noahlan.cn/northlan/ngs/benchmark/testdata" + "git.noahlan.cn/northlan/ngs/component" + "git.noahlan.cn/northlan/ngs/serialize/protobuf" + "git.noahlan.cn/northlan/ngs/session" + "log" + "os" + "os/signal" + "sync/atomic" + "syscall" + "testing" + "time" +) + +const ( + addr = "127.0.0.1:13250" // local address + conc = 1000 // concurrent client count +) + +// +type TestHandler struct { + component.Base + metrics int32 + group *ngs.Group +} + +func (h *TestHandler) AfterInit() { + ticker := time.NewTicker(time.Second) + + // metrics output ticker + go func() { + for range ticker.C { + println("QPS", atomic.LoadInt32(&h.metrics)) + atomic.StoreInt32(&h.metrics, 0) + } + }() +} + +func NewTestHandler() *TestHandler { + return &TestHandler{ + group: ngs.NewGroup("handler"), + } +} + +func (h *TestHandler) Ping(s *session.Session, data *testdata.Ping) error { + atomic.AddInt32(&h.metrics, 1) + return s.Push("pong", &testdata.Pong{Content: data.Content}) +} + +func server() { + components := &component.Components{} + components.Register(NewTestHandler()) + + ngs.Listen(addr, + ngs.WithDebugMode(), + ngs.WithSerializer(protobuf.NewSerializer()), + ngs.WithComponents(components), + ) +} + +func client() { + c := NewClient() + + chReady := make(chan struct{}) + c.OnConnected(func() { + chReady <- struct{}{} + }) + + if err := c.Start(addr); err != nil { + panic(err) + } + + c.On("pong", func(data interface{}) {}) + + <-chReady + for /*i := 0; i < 1; i++*/ { + c.Notify("TestHandler.Ping", &testdata.Ping{}) + time.Sleep(1 * time.Second) + } +} + +func TestIO(t *testing.T) { + go server() + + // wait server startup + time.Sleep(1 * time.Second) + for i := 0; i < conc; i++ { + go client() + } + + log.SetFlags(log.LstdFlags | log.Llongfile) + + sg := make(chan os.Signal) + signal.Notify(sg, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL) + + <-sg + + t.Log("exit") +} diff --git a/examples/cluster/README.md b/examples/cluster/README.md index ff0e583..b1e2a14 100644 --- a/examples/cluster/README.md +++ b/examples/cluster/README.md @@ -1,8 +1,4 @@ -# Nano cluster example - -## About this example - - +# Cluster example ## How to run the example? diff --git a/examples/cluster/chat/chat_service.go b/examples/cluster/chat/chat_service.go index 5c2cd9a..355eec5 100644 --- a/examples/cluster/chat/chat_service.go +++ b/examples/cluster/chat/chat_service.go @@ -1 +1,58 @@ package chat + +import ( + "fmt" + "git.noahlan.cn/northlan/ngs" + "git.noahlan.cn/northlan/ngs/component" + "git.noahlan.cn/northlan/ngs/examples/cluster/protocol" + "git.noahlan.cn/northlan/ngs/internal/log" + "git.noahlan.cn/northlan/ngs/session" +) + +type RoomService struct { + component.Base + group *ngs.Group +} + +func newRoomService() *RoomService { + return &RoomService{ + group: ngs.NewGroup("all-users"), + } +} + +func (rs *RoomService) JoinRoom(s *session.Session, msg *protocol.JoinRoomRequest) error { + if err := s.Bind(msg.MasterUid); err != nil { + return err + } + + broadcast := &protocol.NewUserBroadcast{ + Content: fmt.Sprintf("User user join: %v", msg.Nickname), + } + if err := rs.group.Broadcast("onNewUser", broadcast); err != nil { + return err + } + return rs.group.Add(s) +} + +type SyncMessage struct { + Name string `json:"name"` + Content string `json:"content"` +} + +func (rs *RoomService) SyncMessage(s *session.Session, msg *SyncMessage) error { + // Send an RPC to master server to stats + if err := s.RPC("TopicService.Stats", &protocol.MasterStats{Uid: s.UID()}); err != nil { + return err + } + + // Sync message to all members in this room + return rs.group.Broadcast("onMessage", msg) +} + +func (rs *RoomService) userDisconnected(s *session.Session) { + if err := rs.group.Leave(s); err != nil { + log.Println("Remove user from group failed", s.UID(), err) + return + } + log.Println("User session disconnected", s.UID()) +} diff --git a/examples/cluster/chat/init.go b/examples/cluster/chat/init.go index 5c2cd9a..0c4b725 100644 --- a/examples/cluster/chat/init.go +++ b/examples/cluster/chat/init.go @@ -1 +1,21 @@ package chat + +import ( + "git.noahlan.cn/northlan/ngs/component" + "git.noahlan.cn/northlan/ngs/session" +) + +var ( + // Services is All services in chat server + Services = &component.Components{} + + roomService = newRoomService() +) + +func init() { + Services.Register(roomService) +} + +func OnSessionClosed(s *session.Session) { + roomService.userDisconnected(s) +} diff --git a/examples/cluster/gate/gate_service.go b/examples/cluster/gate/gate_service.go index e76ce25..69e8ebb 100644 --- a/examples/cluster/gate/gate_service.go +++ b/examples/cluster/gate/gate_service.go @@ -1 +1,44 @@ package gate + +import ( + "errors" + "fmt" + "git.noahlan.cn/northlan/ngs/component" + "git.noahlan.cn/northlan/ngs/examples/cluster/protocol" + "git.noahlan.cn/northlan/ngs/session" +) + +type BindService struct { + component.Base + nextGateUid int64 +} + +func newBindService() *BindService { + return &BindService{} +} + +type ( + LoginRequest struct { + Nickname string `json:"nickname"` + } + LoginResponse struct { + Code int `json:"code"` + } +) + +func (bs *BindService) Login(s *session.Session, msg *LoginRequest) error { + bs.nextGateUid++ + uid := bs.nextGateUid + request := &protocol.NewUserRequest{ + Nickname: msg.Nickname, + GateUid: uid, + } + if err := s.RPC("TopicService.NewUser", request); err != nil { + return err + } + return s.Response(&LoginResponse{}) +} + +func (bs *BindService) BindChatServer(s *session.Session, msg []byte) error { + return errors.New(fmt.Sprintf("not implement")) +} diff --git a/examples/cluster/gate/init.go b/examples/cluster/gate/init.go index e76ce25..31e6b87 100644 --- a/examples/cluster/gate/init.go +++ b/examples/cluster/gate/init.go @@ -1 +1,14 @@ package gate + +import "git.noahlan.cn/northlan/ngs/component" + +var ( + // Services is All services in gate server + Services = &component.Components{} + + bindService = newBindService() +) + +func init() { + Services.Register(bindService) +} diff --git a/examples/cluster/main.go b/examples/cluster/main.go index 916b1b5..8815f84 100644 --- a/examples/cluster/main.go +++ b/examples/cluster/main.go @@ -1 +1,179 @@ -package cluster +package main + +import ( + "errors" + "git.noahlan.cn/northlan/ngs" + "git.noahlan.cn/northlan/ngs/examples/cluster/chat" + "git.noahlan.cn/northlan/ngs/examples/cluster/gate" + "git.noahlan.cn/northlan/ngs/examples/cluster/master" + "git.noahlan.cn/northlan/ngs/internal/log" + "git.noahlan.cn/northlan/ngs/serialize/json" + "git.noahlan.cn/northlan/ngs/session" + "github.com/urfave/cli" + "net/http" + "os" + "path/filepath" + "runtime" +) + +func main() { + app := cli.NewApp() + app.Name = "ClusterDemo" + app.Author = "NorthLan" + app.Email = "6995syu@163.com" + app.Description = "cluster demo" + app.Commands = []cli.Command{ + { + Name: "master", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "listen,l", + Usage: "--l --listen :8888 Master service listen address", + Value: "127.0.0.1:34567", + }, + }, + Action: runMaster, + }, + { + Name: "gate", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "master", + Usage: "master server address", + Value: "127.0.0.1:34567", + }, + cli.StringFlag{ + Name: "listen,l", + Usage: "Gate service listen address", + Value: "", + }, + cli.StringFlag{ + Name: "gate-address", + Usage: "Client connect address", + Value: "", + }, + }, + Action: runGate, + }, + { + Name: "chat", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "master", + Usage: "master server address", + Value: "127.0.0.1:34567", + }, + cli.StringFlag{ + Name: "listen,l", + Usage: "Chat service listen address", + Value: "", + }, + }, + Action: runChat, + }, + } + //log.SetFlags(log.LstdFlags | log.Lshortfile) + if err := app.Run(os.Args); err != nil { + log.Fatalf("Startup server error %+v", err) + } +} + +func runMaster(args *cli.Context) error { + listen := args.String("listen") + if listen == "" { + return errors.New("master listen address cannot empty") + } + + webDir := filepath.Join(srcPath(), "master", "web") + log.Println("Ngs master server web content directory", webDir) + log.Println("Ngs master listen address", listen) + log.Println("Open http://127.0.0.1:12345/web/ in browser") + + http.Handle("/web/", http.StripPrefix("/web/", http.FileServer(http.Dir(webDir)))) + + go func() { + if err := http.ListenAndServe(":12345", nil); err != nil { + panic(err) + } + }() + + // Register session closed callback + session.Lifetime.OnClosed(master.OnSessionClosed) + + // Startup Ngs server with the specified listen address + ngs.Listen(listen, + ngs.WithMaster(), + ngs.WithComponents(master.Services), + ngs.WithSerializer(json.NewSerializer()), + ngs.WithDebugMode(), + ) + + return nil +} + +func srcPath() string { + _, file, _, _ := runtime.Caller(0) + return filepath.Dir(file) +} + +func runGate(args *cli.Context) error { + listen := args.String("listen") + if listen == "" { + return errors.New("gate listen address cannot empty") + } + + masterAddr := args.String("master") + if masterAddr == "" { + return errors.New("master address cannot empty") + } + + gateAddr := args.String("gate-address") + if gateAddr == "" { + return errors.New("gate address cannot empty") + } + + log.Println("Current server listen address", listen) + log.Println("Current gate server address", gateAddr) + log.Println("Remote master server address", masterAddr) + + // Startup Ngs server with the specified listen address + ngs.Listen(listen, + ngs.WithAdvertiseAddr(masterAddr), + ngs.WithClientAddr(gateAddr), + ngs.WithComponents(gate.Services), + ngs.WithSerializer(json.NewSerializer()), + ngs.WithIsWebsocket(true), + ngs.WithWSPath("/ngs"), + ngs.WithCheckOriginFunc(func(_ *http.Request) bool { return true }), + ngs.WithDebugMode(), + ) + return nil +} + +func runChat(args *cli.Context) error { + listen := args.String("listen") + if listen == "" { + return errors.New("chat listen address cannot empty") + } + + masterAddr := args.String("master") + if listen == "" { + return errors.New("master address cannot empty") + } + + log.Println("Current chat server listen address", listen) + log.Println("Remote master server address", masterAddr) + + // Register session closed callback + session.Lifetime.OnClosed(chat.OnSessionClosed) + + // Startup Ngs server with the specified listen address + ngs.Listen(listen, + ngs.WithAdvertiseAddr(masterAddr), + ngs.WithComponents(chat.Services), + ngs.WithSerializer(json.NewSerializer()), + ngs.WithDebugMode(), + ) + + return nil +} diff --git a/examples/cluster/master/init.go b/examples/cluster/master/init.go index 06ab7d0..f069554 100644 --- a/examples/cluster/master/init.go +++ b/examples/cluster/master/init.go @@ -1 +1,23 @@ -package main +package master + +import ( + "git.noahlan.cn/northlan/ngs/component" + "git.noahlan.cn/northlan/ngs/session" +) + +var ( + // Services is All services in master server + Services = &component.Components{} + + // topicService Topic service + topicService = newTopicService() + // ... other services +) + +func init() { + Services.Register(topicService) +} + +func OnSessionClosed(s *session.Session) { + topicService.userDisconnected(s) +} diff --git a/examples/cluster/master/topic_service.go b/examples/cluster/master/topic_service.go index 57d5fb2..19e7b7d 100644 --- a/examples/cluster/master/topic_service.go +++ b/examples/cluster/master/topic_service.go @@ -1 +1,90 @@ package master + +import ( + "errors" + "fmt" + "git.noahlan.cn/northlan/ngs/component" + "git.noahlan.cn/northlan/ngs/examples/cluster/protocol" + "git.noahlan.cn/northlan/ngs/internal/log" + "git.noahlan.cn/northlan/ngs/session" + "strings" +) + +type User struct { + session *session.Session + nickname string + gateId int64 + masterId int64 + balance int64 + message int +} + +type TopicService struct { + component.Base + nextUid int64 + users map[int64]*User +} + +func newTopicService() *TopicService { + return &TopicService{ + users: map[int64]*User{}, + } +} + +type ExistsMembersResponse struct { + Members string `json:"members"` +} + +func (ts *TopicService) NewUser(s *session.Session, msg *protocol.NewUserRequest) error { + ts.nextUid++ + uid := ts.nextUid + if err := s.Bind(uid); err != nil { + return err + } + + var members []string + for _, u := range ts.users { + members = append(members, u.nickname) + } + err := s.Push("onMembers", &ExistsMembersResponse{Members: strings.Join(members, ",")}) + if err != nil { + return err + } + + user := &User{ + session: s, + nickname: msg.Nickname, + gateId: msg.GateUid, + masterId: uid, + balance: 1000, + } + ts.users[uid] = user + + chat := &protocol.JoinRoomRequest{ + Nickname: msg.Nickname, + GateUid: msg.GateUid, + MasterUid: uid, + } + return s.RPC("RoomService.JoinRoom", chat) +} + +type UserBalanceResponse struct { + CurrentBalance int64 `json:"currentBalance"` +} + +func (ts *TopicService) Stats(s *session.Session, msg *protocol.MasterStats) error { + // It's OK to use map without lock because of this service running in main thread + user, found := ts.users[msg.Uid] + if !found { + return errors.New(fmt.Sprintf("User not found: %v", msg.Uid)) + } + user.message++ + user.balance-- + return s.Push("onBalance", &UserBalanceResponse{user.balance}) +} + +func (ts *TopicService) userDisconnected(s *session.Session) { + uid := s.UID() + delete(ts.users, uid) + log.Println("User session disconnected", s.UID()) +} diff --git a/examples/cluster/master/web/index.html b/examples/cluster/master/web/index.html index 71e9715..2234b92 100644 --- a/examples/cluster/master/web/index.html +++ b/examples/cluster/master/web/index.html @@ -71,7 +71,7 @@ // gate address var gateHost = "127.0.0.1"; var gatePort = 34590; - starx.init({host: gateHost, port: gatePort, path: '/nano'}, function () { + starx.init({host: gateHost, port: gatePort, path: '/ngs'}, function () { console.log("initialized"); starx.on("onNewUser", onNewUser); starx.on("onMembers", onMembers); diff --git a/examples/singleon/chat/main.go b/examples/singleon/chat/main.go index 9ae2570..a2d66d9 100644 --- a/examples/singleon/chat/main.go +++ b/examples/singleon/chat/main.go @@ -153,7 +153,7 @@ func main() { ngs.WithIsWebsocket(true), ngs.WithPipeline(pip), ngs.WithCheckOriginFunc(func(_ *http.Request) bool { return true }), - ngs.WithWSPath("/nano"), + ngs.WithWSPath("/ngs"), ngs.WithDebugMode(), ngs.WithSerializer(json.NewSerializer()), // override default serializer ngs.WithComponents(components), diff --git a/examples/singleon/chat/web/index.html b/examples/singleon/chat/web/index.html index 36eb6e5..69099de 100644 --- a/examples/singleon/chat/web/index.html +++ b/examples/singleon/chat/web/index.html @@ -58,7 +58,7 @@ v.messages.push({name:'system', content: "members: "+data.members}); }; - starx.init({host: '127.0.0.1', port: 3250, path: '/nano'}, function () { + starx.init({host: '127.0.0.1', port: 3250, path: '/ngs'}, function () { console.log("initialized"); starx.on("onNewUser", onNewUser); starx.on("onMembers", onMembers); diff --git a/go.mod b/go.mod index 21dfac3..54be3da 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,11 @@ require ( ) require ( + github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/urfave/cli v1.22.5 // indirect golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index ceeb0c1..1db0a93 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,9 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -49,9 +52,16 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=