diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 0c5936a5fb..99ca9479a2 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -49,6 +49,7 @@ const ( stopCommand = "stop" monitorCommand = "monitor" leaveCommand = "leave" + statsCommand = "stats" ) const ( @@ -347,6 +348,9 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er case leaveCommand: return i.handleLeave(client, seq) + case statsCommand: + return i.handleStats(client, seq) + default: respHeader := responseHeader{Seq: seq, Error: unsupportedCommand} client.Send(&respHeader, nil) @@ -535,6 +539,16 @@ func (i *AgentRPC) handleLeave(client *rpcClient, seq uint64) error { return err } +// handleStats is used to get various statistics +func (i *AgentRPC) handleStats(client *rpcClient, seq uint64) error { + header := responseHeader{ + Seq: seq, + Error: "", + } + resp := i.agent.Stats() + return client.Send(&header, resp) +} + // Used to convert an error to a string representation func errToString(err error) string { if err == nil { diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index c1f486f9b6..8f22a8fb3f 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -187,6 +187,18 @@ func (c *RPCClient) Leave() error { return c.genericRPC(&header, nil, nil) } +// Stats is used to get debugging state information +func (c *RPCClient) Stats() (map[string]map[string]string, error) { + header := requestHeader{ + Command: statsCommand, + Seq: c.getSeq(), + } + var resp map[string]map[string]string + + err := c.genericRPC(&header, nil, &resp) + return resp, err +} + type monitorHandler struct { client *RPCClient closed bool diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index a0ca5c641a..36c142d0fe 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -262,3 +262,22 @@ OUTER2: t.Fatalf("should log joining") } } + +func TestRPCClientStats(t *testing.T) { + p1 := testRPCClient(t) + defer p1.Close() + testutil.Yield() + + stats, err := p1.client.Stats() + if err != nil { + t.Fatalf("err: %s", err) + } + + if _, ok := stats["agent"]; !ok { + t.Fatalf("bad: %#v", stats) + } + + if _, ok := stats["consul"]; !ok { + t.Fatalf("bad: %#v", stats) + } +} diff --git a/command/info.go b/command/info.go new file mode 100644 index 0000000000..8154d84037 --- /dev/null +++ b/command/info.go @@ -0,0 +1,81 @@ +package command + +import ( + "flag" + "fmt" + "github.com/mitchellh/cli" + "sort" + "strings" +) + +// InfoCommand is a Command implementation that queries a running +// Consul agent for various debugging statistics for operators +type InfoCommand struct { + Ui cli.Ui +} + +func (i *InfoCommand) Help() string { + helpText := ` +Usage: consul info [options] + + Provides debugging information for operators + +Options: + + -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. +` + return strings.TrimSpace(helpText) +} + +func (i *InfoCommand) Run(args []string) int { + cmdFlags := flag.NewFlagSet("info", flag.ContinueOnError) + cmdFlags.Usage = func() { i.Ui.Output(i.Help()) } + rpcAddr := RPCAddrFlag(cmdFlags) + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + client, err := RPCClient(*rpcAddr) + if err != nil { + i.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + defer client.Close() + + stats, err := client.Stats() + if err != nil { + i.Ui.Error(fmt.Sprintf("Error querying agent: %s", err)) + return 1 + } + + // Get the keys in sorted order + keys := make([]string, 0, len(stats)) + for key := range stats { + keys = append(keys, key) + } + sort.Strings(keys) + + // Iterate over each top-level key + for _, key := range keys { + i.Ui.Output(key) + + // Sort the sub-keys + subvals := stats[key] + subkeys := make([]string, 0, len(subvals)) + for k := range subvals { + subkeys = append(subkeys, k) + } + sort.Strings(subkeys) + + // Iterate over the subkeys + for _, subkey := range subkeys { + val := subvals[subkey] + i.Ui.Output(fmt.Sprintf("\t%s = %s", subkey, val)) + } + } + return 0 +} + +func (i *InfoCommand) Synopsis() string { + return "Provides debugging information for operators" +} diff --git a/command/info_test.go b/command/info_test.go new file mode 100644 index 0000000000..91ed721c20 --- /dev/null +++ b/command/info_test.go @@ -0,0 +1,30 @@ +package command + +import ( + "fmt" + "github.com/mitchellh/cli" + "strings" + "testing" +) + +func TestInfoCommand_implements(t *testing.T) { + var _ cli.Command = &InfoCommand{} +} + +func TestInfoCommandRun(t *testing.T) { + a1 := testAgent(t) + defer a1.Shutdown() + + ui := new(cli.MockUi) + c := &InfoCommand{Ui: ui} + args := []string{"-rpc-addr=" + a1.addr} + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + if !strings.Contains(ui.OutputWriter.String(), "agent") { + t.Fatalf("bad: %#v", ui.OutputWriter.String()) + } +} diff --git a/commands.go b/commands.go index c0d1241cf0..59118b01d8 100644 --- a/commands.go +++ b/commands.go @@ -59,6 +59,12 @@ func init() { }, nil }, + "info": func() (cli.Command, error) { + return &command.InfoCommand{ + Ui: ui, + }, nil + }, + "version": func() (cli.Command, error) { return &command.VersionCommand{ Revision: GitCommit,