From 197d62c6ca4324abefe9dbbb92a404d2e74c3fb1 Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Wed, 17 Oct 2018 13:20:35 -0700 Subject: [PATCH] New command: consul debug (#4754) * agent/debug: add package for debugging, host info * api: add v1/agent/host endpoint * agent: add v1/agent/host endpoint * command/debug: implementation of static capture * command/debug: tests and only configured targets * agent/debug: add basic test for host metrics * command/debug: add methods for dynamic data capture * api: add debug/pprof endpoints * command/debug: add pprof * command/debug: timing, wg, logs to disk * vendor: add gopsutil/disk * command/debug: add a usage section * website: add docs for consul debug * agent/host: require operator:read * api/host: improve docs and no retry timing * command/debug: fail on extra arguments * command/debug: fixup file permissions to 0644 * command/debug: remove server flags * command/debug: improve clarity of usage section * api/debug: add Trace for profiling, fix profile * command/debug: capture profile and trace at the same time * command/debug: add index document * command/debug: use "clusters" in place of members * command/debug: remove address in output * command/debug: improve comment on metrics sleep * command/debug: clarify usage * agent: always register pprof handlers and protect This will allow us to avoid a restart of a target agent for profiling by always registering the pprof handlers. Given this is a potentially sensitive path, it is protected with an operator:read ACL and enable debug being set to true on the target agent. enable_debug still requires a restart. If ACLs are disabled, enable_debug is sufficient. * command/debug: use trace.out instead of .prof More in line with golang docs. * agent: fix comment wording * agent: wrap table driven tests in t.run() --- agent/agent_endpoint.go | 25 + agent/agent_endpoint_test.go | 67 +- agent/debug/host.go | 59 ++ agent/debug/host_test.go | 20 + agent/http.go | 48 +- agent/http_oss.go | 1 + agent/http_test.go | 100 +++ api/agent.go | 18 + api/agent_test.go | 20 + api/debug.go | 106 +++ api/debug_test.go | 83 +++ command/commands_oss.go | 2 + command/debug/debug.go | 702 ++++++++++++++++++ command/debug/debug_test.go | 482 ++++++++++++ testutil/server.go | 1 + vendor/github.com/shirou/gopsutil/LICENSE | 61 ++ .../github.com/shirou/gopsutil/disk/disk.go | 64 ++ .../shirou/gopsutil/disk/disk_darwin.go | 111 +++ .../shirou/gopsutil/disk/disk_darwin_amd64.go | 58 ++ .../shirou/gopsutil/disk/disk_darwin_arm64.go | 58 ++ .../shirou/gopsutil/disk/disk_fallback.go | 17 + .../shirou/gopsutil/disk/disk_freebsd.go | 176 +++++ .../shirou/gopsutil/disk/disk_freebsd_386.go | 112 +++ .../gopsutil/disk/disk_freebsd_amd64.go | 115 +++ .../shirou/gopsutil/disk/disk_linux.go | 393 ++++++++++ .../shirou/gopsutil/disk/disk_openbsd.go | 158 ++++ .../gopsutil/disk/disk_openbsd_amd64.go | 91 +++ .../shirou/gopsutil/disk/disk_unix.go | 45 ++ .../shirou/gopsutil/disk/disk_windows.go | 155 ++++ vendor/vendor.json | 1 + website/source/docs/agent/options.html.md | 3 +- .../docs/commands/debug.html.markdown.erb | 122 +++ website/source/layouts/docs.erb | 3 + 33 files changed, 3463 insertions(+), 14 deletions(-) create mode 100644 agent/debug/host.go create mode 100644 agent/debug/host_test.go create mode 100644 api/debug.go create mode 100644 api/debug_test.go create mode 100644 command/debug/debug.go create mode 100644 command/debug/debug_test.go create mode 100644 vendor/github.com/shirou/gopsutil/LICENSE create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_darwin.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_darwin_amd64.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_darwin_arm64.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_fallback.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_freebsd.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_freebsd_386.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_freebsd_amd64.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_linux.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_openbsd.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_openbsd_amd64.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_unix.go create mode 100644 vendor/github.com/shirou/gopsutil/disk/disk_windows.go create mode 100644 website/source/docs/commands/debug.html.markdown.erb diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 853b4dcdd..9092dbe48 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/config" + "github.com/hashicorp/consul/agent/debug" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/ipaddr" @@ -1463,3 +1464,27 @@ type connectAuthorizeResp struct { Authorized bool // True if authorized, false if not Reason string // Reason for the Authorized value (whether true or false) } + +// AgentHost +// +// GET /v1/agent/host +// +// Retrieves information about resources available and in-use for the +// host the agent is running on such as CPU, memory, and disk usage. Requires +// a operator:read ACL token. +func (s *HTTPServer) AgentHost(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + // Fetch the ACL token, if any, and enforce agent policy. + var token string + s.parseToken(req, &token) + rule, err := s.agent.resolveToken(token) + if err != nil { + return nil, err + } + // TODO(pearkes): Is agent:read appropriate here? There could be relatively + // sensitive information made available in this API + if rule != nil && !rule.OperatorRead() { + return nil, acl.ErrPermissionDenied + } + + return debug.CollectHostInfo(), nil +} diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 3a13135f5..f353360bb 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/connect" + "github.com/hashicorp/consul/agent/debug" "github.com/hashicorp/consul/agent/local" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" @@ -1877,9 +1878,9 @@ func TestAgent_RegisterService_TranslateKeys(t *testing.T) { json := ` { - "name":"test", - "port":8000, - "enable_tag_override": true, + "name":"test", + "port":8000, + "enable_tag_override": true, "meta": { "some": "meta", "enable_tag_override": "meta is 'opaque' so should not get translated" @@ -1929,9 +1930,9 @@ func TestAgent_RegisterService_TranslateKeys(t *testing.T) { ] }, "sidecar_service": { - "name":"test-proxy", - "port":8001, - "enable_tag_override": true, + "name":"test-proxy", + "port":8001, + "enable_tag_override": true, "meta": { "some": "meta", "enable_tag_override": "sidecar_service.meta is 'opaque' so should not get translated" @@ -2791,7 +2792,7 @@ func TestAgent_RegisterServiceDeregisterService_Sidecar(t *testing.T) { require := require.New(t) // Constrain auto ports to 1 available to make it deterministic - hcl := `ports { + hcl := `ports { sidecar_min_port = 2222 sidecar_max_port = 2222 } @@ -5537,3 +5538,55 @@ func testAllowProxyConfig() string { } ` } + +func TestAgent_Host(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + dc1 := "dc1" + a := NewTestAgent(t.Name(), ` + acl_datacenter = "`+dc1+`" + acl_default_policy = "allow" + acl_master_token = "master" + acl_agent_token = "agent" + acl_agent_master_token = "towel" + acl_enforce_version_8 = true +`) + defer a.Shutdown() + + testrpc.WaitForLeader(t, a.RPC, "dc1") + req, _ := http.NewRequest("GET", "/v1/agent/host?token=master", nil) + resp := httptest.NewRecorder() + respRaw, err := a.srv.AgentHost(resp, req) + assert.Nil(err) + assert.Equal(http.StatusOK, resp.Code) + assert.NotNil(respRaw) + + obj := respRaw.(*debug.HostInfo) + assert.NotNil(obj.CollectionTime) + assert.Empty(obj.Errors) +} + +func TestAgent_HostBadACL(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + dc1 := "dc1" + a := NewTestAgent(t.Name(), ` + acl_datacenter = "`+dc1+`" + acl_default_policy = "deny" + acl_master_token = "root" + acl_agent_token = "agent" + acl_agent_master_token = "towel" + acl_enforce_version_8 = true +`) + defer a.Shutdown() + + testrpc.WaitForLeader(t, a.RPC, "dc1") + req, _ := http.NewRequest("GET", "/v1/agent/host?token=agent", nil) + resp := httptest.NewRecorder() + respRaw, err := a.srv.AgentHost(resp, req) + assert.EqualError(err, "ACL not found") + assert.Equal(http.StatusOK, resp.Code) + assert.Nil(respRaw) +} diff --git a/agent/debug/host.go b/agent/debug/host.go new file mode 100644 index 000000000..bbc242576 --- /dev/null +++ b/agent/debug/host.go @@ -0,0 +1,59 @@ +package debug + +import ( + "time" + + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/mem" +) + +const ( + // DiskUsagePath is the path to check usage of the disk. + // Must be a filessytem path such as "/", not device file path like "/dev/vda1" + DiskUsagePath = "/" +) + +// HostInfo includes information about resources on the host as well as +// collection time and +type HostInfo struct { + Memory *mem.VirtualMemoryStat + CPU []cpu.InfoStat + Host *host.InfoStat + Disk *disk.UsageStat + CollectionTime int64 + Errors []error +} + +// CollectHostInfo queries the host system and returns HostInfo. Any +// errors encountered will be returned in HostInfo.Errors +func CollectHostInfo() *HostInfo { + info := &HostInfo{CollectionTime: time.Now().UTC().UnixNano()} + + if h, err := host.Info(); err != nil { + info.Errors = append(info.Errors, err) + } else { + info.Host = h + } + + if v, err := mem.VirtualMemory(); err != nil { + info.Errors = append(info.Errors, err) + } else { + info.Memory = v + } + + if d, err := disk.Usage(DiskUsagePath); err != nil { + info.Errors = append(info.Errors, err) + } else { + info.Disk = d + } + + if c, err := cpu.Info(); err != nil { + info.Errors = append(info.Errors, err) + } else { + info.CPU = c + } + + return info +} diff --git a/agent/debug/host_test.go b/agent/debug/host_test.go new file mode 100644 index 000000000..0a26094e5 --- /dev/null +++ b/agent/debug/host_test.go @@ -0,0 +1,20 @@ +package debug + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCollectHostInfo(t *testing.T) { + assert := assert.New(t) + + host := CollectHostInfo() + + assert.Nil(host.Errors) + + assert.NotNil(host.CollectionTime) + assert.NotNil(host.Host) + assert.NotNil(host.Disk) + assert.NotNil(host.Memory) +} diff --git a/agent/http.go b/agent/http.go index b32ce1ec0..5cb6d1bfa 100644 --- a/agent/http.go +++ b/agent/http.go @@ -142,6 +142,41 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { mux.Handle(pattern, gzipHandler) } + // handlePProf takes the given pattern and pprof handler + // and wraps it to add authorization and metrics + handlePProf := func(pattern string, handler http.HandlerFunc) { + wrapper := func(resp http.ResponseWriter, req *http.Request) { + var token string + s.parseToken(req, &token) + + rule, err := s.agent.resolveToken(token) + if err != nil { + resp.WriteHeader(http.StatusForbidden) + return + } + + // If enableDebug is not set, and ACLs are disabled, write + // an unauthorized response + if !enableDebug { + if s.checkACLDisabled(resp, req) { + return + } + } + + // If the token provided does not have the necessary permissions, + // write a forbidden response + if rule != nil && !rule.OperatorRead() { + resp.WriteHeader(http.StatusForbidden) + return + } + + // Call the pprof handler + handler(resp, req) + } + + handleFuncMetrics(pattern, http.HandlerFunc(wrapper)) + } + mux.HandleFunc("/", s.Index) for pattern, fn := range endpoints { thisFn := fn @@ -151,12 +186,13 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { } handleFuncMetrics(pattern, s.wrap(bound, methods)) } - if enableDebug { - handleFuncMetrics("/debug/pprof/", pprof.Index) - handleFuncMetrics("/debug/pprof/cmdline", pprof.Cmdline) - handleFuncMetrics("/debug/pprof/profile", pprof.Profile) - handleFuncMetrics("/debug/pprof/symbol", pprof.Symbol) - } + + // Register wrapped pprof handlers + handlePProf("/debug/pprof/", pprof.Index) + handlePProf("/debug/pprof/cmdline", pprof.Cmdline) + handlePProf("/debug/pprof/profile", pprof.Profile) + handlePProf("/debug/pprof/symbol", pprof.Symbol) + handlePProf("/debug/pprof/trace", pprof.Trace) if s.IsUIEnabled() { legacy_ui, err := strconv.ParseBool(os.Getenv("CONSUL_UI_LEGACY")) diff --git a/agent/http_oss.go b/agent/http_oss.go index fb708f27e..1afdc9a24 100644 --- a/agent/http_oss.go +++ b/agent/http_oss.go @@ -13,6 +13,7 @@ func init() { registerEndpoint("/v1/acl/replication", []string{"GET"}, (*HTTPServer).ACLReplicationStatus) registerEndpoint("/v1/agent/token/", []string{"PUT"}, (*HTTPServer).AgentToken) registerEndpoint("/v1/agent/self", []string{"GET"}, (*HTTPServer).AgentSelf) + registerEndpoint("/v1/agent/host", []string{"GET"}, (*HTTPServer).AgentHost) registerEndpoint("/v1/agent/maintenance", []string{"PUT"}, (*HTTPServer).AgentNodeMaintenance) registerEndpoint("/v1/agent/reload", []string{"PUT"}, (*HTTPServer).AgentReload) registerEndpoint("/v1/agent/monitor", []string{"GET"}, (*HTTPServer).AgentMonitor) diff --git a/agent/http_test.go b/agent/http_test.go index 57d65ab4d..6bb269630 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -20,8 +20,10 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/testutil" "github.com/hashicorp/go-cleanhttp" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/http2" ) @@ -724,6 +726,104 @@ func TestParseWait(t *testing.T) { t.Fatalf("Bad: %v", b) } } +func TestPProfHandlers_EnableDebug(t *testing.T) { + t.Parallel() + require := require.New(t) + a := NewTestAgent(t.Name(), "enable_debug = true") + defer a.Shutdown() + + resp := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/debug/pprof/profile", nil) + + a.srv.Handler.ServeHTTP(resp, req) + + require.Equal(http.StatusOK, resp.Code) +} +func TestPProfHandlers_DisableDebugNoACLs(t *testing.T) { + t.Parallel() + require := require.New(t) + a := NewTestAgent(t.Name(), "enable_debug = false") + defer a.Shutdown() + + resp := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/debug/pprof/profile", nil) + + a.srv.Handler.ServeHTTP(resp, req) + + require.Equal(http.StatusUnauthorized, resp.Code) +} + +func TestPProfHandlers_ACLs(t *testing.T) { + t.Parallel() + assert := assert.New(t) + dc1 := "dc1" + + a := NewTestAgent(t.Name(), ` + acl_datacenter = "`+dc1+`" + acl_default_policy = "deny" + acl_master_token = "master" + acl_agent_token = "agent" + acl_agent_master_token = "towel" + acl_enforce_version_8 = true + enable_debug = false +`) + + cases := []struct { + code int + token string + endpoint string + nilResponse bool + }{ + { + code: http.StatusOK, + token: "master", + endpoint: "/debug/pprof/heap", + nilResponse: false, + }, + { + code: http.StatusForbidden, + token: "agent", + endpoint: "/debug/pprof/heap", + nilResponse: true, + }, + { + code: http.StatusForbidden, + token: "agent", + endpoint: "/debug/pprof/", + nilResponse: true, + }, + { + code: http.StatusForbidden, + token: "", + endpoint: "/debug/pprof/", + nilResponse: true, + }, + { + code: http.StatusOK, + token: "master", + endpoint: "/debug/pprof/heap", + nilResponse: false, + }, + { + code: http.StatusForbidden, + token: "towel", + endpoint: "/debug/pprof/heap", + nilResponse: true, + }, + } + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + for i, c := range cases { + t.Run(fmt.Sprintf("case %d (%#v)", i, c), func(t *testing.T) { + req, _ := http.NewRequest("GET", fmt.Sprintf("%s?token=%s", c.endpoint, c.token), nil) + resp := httptest.NewRecorder() + a.srv.Handler.ServeHTTP(resp, req) + assert.Equal(c.code, resp.Code) + }) + } +} func TestParseWait_InvalidTime(t *testing.T) { t.Parallel() diff --git a/api/agent.go b/api/agent.go index d6002f583..8e5ffde30 100644 --- a/api/agent.go +++ b/api/agent.go @@ -314,6 +314,24 @@ func (a *Agent) Self() (map[string]map[string]interface{}, error) { return out, nil } +// Host is used to retrieve information about the host the +// agent is running on such as CPU, memory, and disk. Requires +// a operator:read ACL token. +func (a *Agent) Host() (map[string]interface{}, error) { + r := a.c.newRequest("GET", "/v1/agent/host") + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out map[string]interface{} + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return out, nil +} + // Metrics is used to query the agent we are speaking to for // its current internal metric data func (a *Agent) Metrics() (*MetricsInfo, error) { diff --git a/api/agent_test.go b/api/agent_test.go index 94b0ee7a9..cc32e7ae7 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -53,6 +53,26 @@ func TestAPI_AgentMetrics(t *testing.T) { }) } +func TestAPI_AgentHost(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + agent := c.Agent() + timer := &retry.Timer{} + retry.RunWith(timer, t, func(r *retry.R) { + host, err := agent.Host() + if err != nil { + r.Fatalf("err: %v", err) + } + + // CollectionTime should exist on all responses + if host["CollectionTime"] == nil { + r.Fatalf("missing host response") + } + }) +} + func TestAPI_AgentReload(t *testing.T) { t.Parallel() diff --git a/api/debug.go b/api/debug.go new file mode 100644 index 000000000..238046853 --- /dev/null +++ b/api/debug.go @@ -0,0 +1,106 @@ +package api + +import ( + "fmt" + "io/ioutil" + "strconv" +) + +// Debug can be used to query the /debug/pprof endpoints to gather +// profiling information about the target agent.Debug +// +// The agent must have enable_debug set to true for profiling to be enabled +// and for these endpoints to function. +type Debug struct { + c *Client +} + +// Debug returns a handle that exposes the internal debug endpoints. +func (c *Client) Debug() *Debug { + return &Debug{c} +} + +// Heap returns a pprof heap dump +func (d *Debug) Heap() ([]byte, error) { + r := d.c.newRequest("GET", "/debug/pprof/heap") + _, resp, err := d.c.doRequest(r) + if err != nil { + return nil, fmt.Errorf("error making request: %s", err) + } + defer resp.Body.Close() + + // We return a raw response because we're just passing through a response + // from the pprof handlers + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error decoding body: %s", err) + } + + return body, nil +} + +// Profile returns a pprof CPU profile for the specified number of seconds +func (d *Debug) Profile(seconds int) ([]byte, error) { + r := d.c.newRequest("GET", "/debug/pprof/profile") + + // Capture a profile for the specified number of seconds + r.params.Set("seconds", strconv.Itoa(seconds)) + + _, resp, err := d.c.doRequest(r) + if err != nil { + return nil, fmt.Errorf("error making request: %s", err) + } + defer resp.Body.Close() + + // We return a raw response because we're just passing through a response + // from the pprof handlers + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error decoding body: %s", err) + } + + return body, nil +} + +// Trace returns an execution trace +func (d *Debug) Trace(seconds int) ([]byte, error) { + r := d.c.newRequest("GET", "/debug/pprof/trace") + + // Capture a trace for the specified number of seconds + r.params.Set("seconds", strconv.Itoa(seconds)) + + _, resp, err := d.c.doRequest(r) + if err != nil { + return nil, fmt.Errorf("error making request: %s", err) + } + defer resp.Body.Close() + + // We return a raw response because we're just passing through a response + // from the pprof handlers + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error decoding body: %s", err) + } + + return body, nil +} + +// Goroutine returns a pprof goroutine profile +func (d *Debug) Goroutine() ([]byte, error) { + r := d.c.newRequest("GET", "/debug/pprof/goroutine") + + _, resp, err := d.c.doRequest(r) + if err != nil { + return nil, fmt.Errorf("error making request: %s", err) + } + defer resp.Body.Close() + + // We return a raw response because we're just passing through a response + // from the pprof handlers + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error decoding body: %s", err) + } + + return body, nil +} diff --git a/api/debug_test.go b/api/debug_test.go new file mode 100644 index 000000000..cf0a9287a --- /dev/null +++ b/api/debug_test.go @@ -0,0 +1,83 @@ +package api + +import ( + "testing" + + "github.com/hashicorp/consul/testutil" +) + +func TestAPI_DebugHeap(t *testing.T) { + t.Parallel() + c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) { + conf.EnableDebug = true + }) + + defer s.Stop() + + debug := c.Debug() + raw, err := debug.Heap() + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(raw) <= 0 { + t.Fatalf("no response: %#v", raw) + } +} + +func TestAPI_DebugProfile(t *testing.T) { + t.Parallel() + c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) { + conf.EnableDebug = true + }) + + defer s.Stop() + + debug := c.Debug() + raw, err := debug.Profile(1) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(raw) <= 0 { + t.Fatalf("no response: %#v", raw) + } +} + +func TestAPI_DebugGoroutine(t *testing.T) { + t.Parallel() + c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) { + conf.EnableDebug = true + }) + + defer s.Stop() + + debug := c.Debug() + raw, err := debug.Goroutine() + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(raw) <= 0 { + t.Fatalf("no response: %#v", raw) + } +} + +func TestAPI_DebugTrace(t *testing.T) { + t.Parallel() + c, s := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) { + conf.EnableDebug = true + }) + + defer s.Stop() + + debug := c.Debug() + raw, err := debug.Trace(1) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(raw) <= 0 { + t.Fatalf("no response: %#v", raw) + } +} diff --git a/command/commands_oss.go b/command/commands_oss.go index a96d90ffe..c53dd1a49 100644 --- a/command/commands_oss.go +++ b/command/commands_oss.go @@ -12,6 +12,7 @@ import ( caset "github.com/hashicorp/consul/command/connect/ca/set" "github.com/hashicorp/consul/command/connect/envoy" "github.com/hashicorp/consul/command/connect/proxy" + "github.com/hashicorp/consul/command/debug" "github.com/hashicorp/consul/command/event" "github.com/hashicorp/consul/command/exec" "github.com/hashicorp/consul/command/forceleave" @@ -79,6 +80,7 @@ func init() { Register("connect ca set-config", func(ui cli.Ui) (cli.Command, error) { return caset.New(ui), nil }) Register("connect proxy", func(ui cli.Ui) (cli.Command, error) { return proxy.New(ui, MakeShutdownCh()), nil }) Register("connect envoy", func(ui cli.Ui) (cli.Command, error) { return envoy.New(ui), nil }) + Register("debug", func(ui cli.Ui) (cli.Command, error) { return debug.New(ui, MakeShutdownCh()), nil }) Register("event", func(ui cli.Ui) (cli.Command, error) { return event.New(ui), nil }) Register("exec", func(ui cli.Ui) (cli.Command, error) { return exec.New(ui, MakeShutdownCh()), nil }) Register("force-leave", func(ui cli.Ui) (cli.Command, error) { return forceleave.New(ui), nil }) diff --git a/command/debug/debug.go b/command/debug/debug.go new file mode 100644 index 000000000..2091bc34f --- /dev/null +++ b/command/debug/debug.go @@ -0,0 +1,702 @@ +package debug + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + multierror "github.com/hashicorp/go-multierror" + "github.com/mitchellh/cli" +) + +const ( + // debugInterval is the interval in which to capture dynamic information + // when running debug + debugInterval = 30 * time.Second + + // debugDuration is the total duration that debug runs before being + // shut down + debugDuration = 2 * time.Minute + + // debugDurationGrace is a period of time added to the specified + // duration to allow intervals to capture within that time + debugDurationGrace = 2 * time.Second + + // debugMinInterval is the minimum a user can configure the interval + // to prevent accidental DOS + debugMinInterval = 5 * time.Second + + // debugMinDuration is the minimum a user can configure the duration + // to ensure that all information can be collected in time + debugMinDuration = 10 * time.Second + + // debugArchiveExtension is the extension for archive files + debugArchiveExtension = ".tar.gz" + + // debugProtocolVersion is the version of the package that is + // generated. If this format changes interface, this version + // can be incremented so clients can selectively support packages + debugProtocolVersion = 1 +) + +func New(ui cli.Ui, shutdownCh <-chan struct{}) *cmd { + ui = &cli.PrefixedUi{ + OutputPrefix: "==> ", + InfoPrefix: " ", + ErrorPrefix: "==> ", + Ui: ui, + } + + c := &cmd{UI: ui, shutdownCh: shutdownCh} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + shutdownCh <-chan struct{} + + // flags + interval time.Duration + duration time.Duration + output string + archive bool + capture []string + client *api.Client + // validateTiming can be used to skip validation of interval, duration. This + // is primarily useful for testing + validateTiming bool + + index *debugIndex +} + +// debugIndex is used to manage the summary of all data recorded +// during the debug, to be written to json at the end of the run +// and stored at the root. Each attribute corresponds to a file or files. +type debugIndex struct { + // Version of the debug package + Version int + // Version of the target Consul agent + AgentVersion string + + Interval string + Duration string + + Targets []string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + defaultFilename := fmt.Sprintf("consul-debug-%d", time.Now().Unix()) + + c.flags.Var((*flags.AppendSliceValue)(&c.capture), "capture", + fmt.Sprintf("One or more types of information to capture. This can be used "+ + "to capture a subset of information, and defaults to capturing "+ + "everything available. Possible information for capture: %s. "+ + "This can be repeated multiple times.", strings.Join(c.defaultTargets(), ", "))) + c.flags.DurationVar(&c.interval, "interval", debugInterval, + fmt.Sprintf("The interval in which to capture dynamic information such as "+ + "telemetry, and profiling. Defaults to %s.", debugInterval)) + c.flags.DurationVar(&c.duration, "duration", debugDuration, + fmt.Sprintf("The total time to record information. "+ + "Defaults to %s.", debugDuration)) + c.flags.BoolVar(&c.archive, "archive", true, "Boolean value for if the files "+ + "should be archived and compressed. Setting this to false will skip the "+ + "archive step and leave the directory of information on the current path.") + c.flags.StringVar(&c.output, "output", defaultFilename, "The path "+ + "to the compressed archive that will be created with the "+ + "information after collection.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + c.help = flags.Usage(help, c.flags) + + c.validateTiming = true +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + c.UI.Error(fmt.Sprintf("Error parsing flags: %s", err)) + return 1 + } + + if len(c.flags.Args()) > 0 { + c.UI.Error("debug: Too many arguments provided, expected 0") + return 1 + } + + // Connect to the agent + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + c.client = client + + version, err := c.prepare() + if err != nil { + c.UI.Error(fmt.Sprintf("Capture validation failed: %v", err)) + return 1 + } + + archiveName := c.output + // Show the user the final file path if archiving + if c.archive { + archiveName = archiveName + debugArchiveExtension + } + + c.UI.Output("Starting debugger and capturing static information...") + + // Output metadata about target agent + c.UI.Info(fmt.Sprintf(" Agent Version: '%s'", version)) + c.UI.Info(fmt.Sprintf(" Interval: '%s'", c.interval)) + c.UI.Info(fmt.Sprintf(" Duration: '%s'", c.duration)) + c.UI.Info(fmt.Sprintf(" Output: '%s'", archiveName)) + c.UI.Info(fmt.Sprintf(" Capture: '%s'", strings.Join(c.capture, ", "))) + + // Record some information for the index at the root of the archive + index := &debugIndex{ + Version: debugProtocolVersion, + AgentVersion: version, + Interval: c.interval.String(), + Duration: c.duration.String(), + Targets: c.capture, + } + + // Add the extra grace period to ensure + // all intervals will be captured within the time allotted + c.duration = c.duration + debugDurationGrace + + // Capture static information from the target agent + err = c.captureStatic() + if err != nil { + c.UI.Warn(fmt.Sprintf("Static capture failed: %v", err)) + } + + // Capture dynamic information from the target agent, blocking for duration + if c.configuredTarget("metrics") || c.configuredTarget("logs") || c.configuredTarget("pprof") { + err = c.captureDynamic() + if err != nil { + c.UI.Error(fmt.Sprintf("Error encountered during collection: %v", err)) + } + } + + // Write the index document + idxMarshalled, err := json.MarshalIndent(index, "", "\t") + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshalling index document: %v", err)) + return 1 + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/index.json", c.output), idxMarshalled, 0644) + if err != nil { + c.UI.Error(fmt.Sprintf("Error creating index document: %v", err)) + return 1 + } + + // Archive the data if configured to + if c.archive { + err = c.createArchive() + + if err != nil { + c.UI.Warn(fmt.Sprintf("Archive creation failed: %v", err)) + return 1 + } + } + + c.UI.Info(fmt.Sprintf("Saved debug archive: %s", archiveName)) + + return 0 +} + +// prepare validates agent settings against targets and prepares the environment for capturing +func (c *cmd) prepare() (version string, err error) { + // Ensure realistic duration and intervals exists + if c.validateTiming { + if c.duration < debugMinDuration { + return "", fmt.Errorf("duration must be longer than %s", debugMinDuration) + } + + if c.interval < debugMinInterval { + return "", fmt.Errorf("interval must be longer than %s", debugMinDuration) + } + + if c.duration < c.interval { + return "", fmt.Errorf("duration (%s) must be longer than interval (%s)", c.duration, c.interval) + } + } + + // Retrieve and process agent information necessary to validate + self, err := c.client.Agent().Self() + if err != nil { + return "", fmt.Errorf("error querying target agent: %s. verify connectivity and agent address", err) + } + + version, ok := self["Config"]["Version"].(string) + if !ok { + return "", fmt.Errorf("agent response did not contain version key") + } + + debugEnabled, ok := self["DebugConfig"]["EnableDebug"].(bool) + if !ok { + return version, fmt.Errorf("agent response did not contain debug key") + } + + // If none are specified we will collect information from + // all by default + if len(c.capture) == 0 { + c.capture = c.defaultTargets() + } + + if !debugEnabled && c.configuredTarget("pprof") { + cs := c.capture + for i := 0; i < len(cs); i++ { + if cs[i] == "pprof" { + c.capture = append(cs[:i], cs[i+1:]...) + i-- + } + } + c.UI.Warn("[WARN] Unable to capture pprof. Set enable_debug to true on target agent to enable profiling.") + } + + for _, t := range c.capture { + if !c.allowedTarget(t) { + return version, fmt.Errorf("target not found: %s", t) + } + } + + if _, err := os.Stat(c.output); os.IsNotExist(err) { + err := os.MkdirAll(c.output, 0755) + if err != nil { + return version, fmt.Errorf("could not create output directory: %s", err) + } + } else { + return version, fmt.Errorf("output directory already exists: %s", c.output) + } + + return version, nil +} + +// captureStatic captures static target information and writes it +// to the output path +func (c *cmd) captureStatic() error { + // Collect errors via multierror as we want to gracefully + // fail if an API is inacessible + var errors error + + // Collect the named outputs here + outputs := make(map[string]interface{}, 0) + + // Capture host information + if c.configuredTarget("host") { + host, err := c.client.Agent().Host() + if err != nil { + errors = multierror.Append(errors, err) + } + outputs["host"] = host + } + + // Capture agent information + if c.configuredTarget("agent") { + agent, err := c.client.Agent().Self() + if err != nil { + errors = multierror.Append(errors, err) + } + outputs["agent"] = agent + } + + // Capture cluster members information, including WAN + if c.configuredTarget("cluster") { + members, err := c.client.Agent().Members(true) + if err != nil { + errors = multierror.Append(errors, err) + } + outputs["cluster"] = members + } + + // Write all outputs to disk as JSON + for output, v := range outputs { + marshaled, err := json.MarshalIndent(v, "", "\t") + if err != nil { + errors = multierror.Append(errors, err) + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/%s.json", c.output, output), marshaled, 0644) + if err != nil { + errors = multierror.Append(errors, err) + } + } + + return errors +} + +// captureDynamic blocks for the duration of the command +// specified by the duration flag, capturing the dynamic +// targets at the interval specified +func (c *cmd) captureDynamic() error { + successChan := make(chan int64) + errCh := make(chan error) + durationChn := time.After(c.duration) + intervalCount := 0 + + c.UI.Output(fmt.Sprintf("Beginning capture interval %s (%d)", time.Now().Local().String(), intervalCount)) + + // We'll wait for all of the targets configured to be + // captured before continuing + var wg sync.WaitGroup + + capture := func() { + timestamp := time.Now().Local().Unix() + + // Make the directory that will store all captured data + // for this interval + timestampDir := fmt.Sprintf("%s/%d", c.output, timestamp) + err := os.MkdirAll(timestampDir, 0755) + if err != nil { + errCh <- err + } + + // Capture metrics + if c.configuredTarget("metrics") { + wg.Add(1) + + go func() { + metrics, err := c.client.Agent().Metrics() + if err != nil { + errCh <- err + } + + marshaled, err := json.MarshalIndent(metrics, "", "\t") + if err != nil { + errCh <- err + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/%s.json", timestampDir, "metrics"), marshaled, 0644) + if err != nil { + errCh <- err + } + + // We need to sleep for the configured interval in the case + // of metrics being the only target captured. When it is, + // the waitgroup would return on Wait() and repeat without + // waiting for the interval. + time.Sleep(c.interval) + + wg.Done() + }() + } + + // Capture pprof + if c.configuredTarget("pprof") { + wg.Add(1) + + go func() { + // We need to capture profiles and traces at the same time + // and block for both of them + var wgProf sync.WaitGroup + + heap, err := c.client.Debug().Heap() + if err != nil { + errCh <- err + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/heap.prof", timestampDir), heap, 0644) + if err != nil { + errCh <- err + } + + // Capture a profile/trace with a minimum of 1s + s := c.interval.Seconds() + if s < 1 { + s = 1 + } + + go func() { + wgProf.Add(1) + + prof, err := c.client.Debug().Profile(int(s)) + if err != nil { + errCh <- err + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/profile.prof", timestampDir), prof, 0644) + if err != nil { + errCh <- err + } + + wgProf.Done() + }() + + go func() { + wgProf.Add(1) + + trace, err := c.client.Debug().Trace(int(s)) + if err != nil { + errCh <- err + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/trace.out", timestampDir), trace, 0644) + if err != nil { + errCh <- err + } + + wgProf.Done() + }() + + gr, err := c.client.Debug().Goroutine() + if err != nil { + errCh <- err + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/goroutine.prof", timestampDir), gr, 0644) + if err != nil { + errCh <- err + } + + wgProf.Wait() + + wg.Done() + }() + } + + // Capture logs + if c.configuredTarget("logs") { + wg.Add(1) + + go func() { + endLogChn := make(chan struct{}) + logCh, err := c.client.Agent().Monitor("DEBUG", endLogChn, nil) + if err != nil { + errCh <- err + } + // Close the log stream + defer close(endLogChn) + + // Create the log file for writing + f, err := os.Create(fmt.Sprintf("%s/%s", timestampDir, "consul.log")) + if err != nil { + errCh <- err + } + defer f.Close() + + intervalChn := time.After(c.interval) + + OUTER: + + for { + select { + case log := <-logCh: + // Append the line to the file + if _, err = f.WriteString(log + "\n"); err != nil { + errCh <- err + break OUTER + } + // Stop collecting the logs after the interval specified + case <-intervalChn: + break OUTER + } + } + + wg.Done() + }() + } + + // Wait for all captures to complete + wg.Wait() + + // Send down the timestamp for UI output + successChan <- timestamp + } + + go capture() + + for { + select { + case t := <-successChan: + intervalCount++ + c.UI.Output(fmt.Sprintf("Capture successful %s (%d)", time.Unix(t, 0).Local().String(), intervalCount)) + go capture() + case e := <-errCh: + c.UI.Error(fmt.Sprintf("Capture failure %s", e)) + case <-durationChn: + return nil + case <-c.shutdownCh: + return errors.New("stopping collection due to shutdown signal") + } + } +} + +// allowedTarget returns a boolean if the target is able to be captured +func (c *cmd) allowedTarget(target string) bool { + for _, dt := range c.defaultTargets() { + if dt == target { + return true + } + } + return false +} + +// configuredTarget returns a boolean if the target is configured to be +// captured in the command +func (c *cmd) configuredTarget(target string) bool { + for _, dt := range c.capture { + if dt == target { + return true + } + } + return false +} + +// createArchive walks the files in the temporary directory +// and creates a tar file that is gzipped with the contents +func (c *cmd) createArchive() error { + f, err := os.Create(c.output + debugArchiveExtension) + if err != nil { + return fmt.Errorf("failed to create compressed archive: %s", err) + } + defer f.Close() + + g := gzip.NewWriter(f) + defer g.Close() + t := tar.NewWriter(f) + defer t.Close() + + err = filepath.Walk(c.output, func(file string, fi os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("failed to walk filepath for archive: %s", err) + } + + header, err := tar.FileInfoHeader(fi, fi.Name()) + if err != nil { + return fmt.Errorf("failed to create compressed archive header: %s", err) + } + + header.Name = filepath.Join(filepath.Base(c.output), strings.TrimPrefix(file, c.output)) + + if err := t.WriteHeader(header); err != nil { + return fmt.Errorf("failed to write compressed archive header: %s", err) + } + + // Only copy files + if !fi.Mode().IsRegular() { + return nil + } + + f, err := os.Open(file) + if err != nil { + return fmt.Errorf("failed to open target files for archive: %s", err) + } + + if _, err := io.Copy(t, f); err != nil { + return fmt.Errorf("failed to copy files for archive: %s", err) + } + + f.Close() + + return nil + }) + + if err != nil { + return fmt.Errorf("failed to walk output path for archive: %s", err) + } + + // Remove directory that has been archived + err = os.RemoveAll(c.output) + if err != nil { + return fmt.Errorf("failed to remove archived directory: %s", err) + } + + return nil +} + +// defaultTargets specifies the list of all targets that +// will be captured by default +func (c *cmd) defaultTargets() []string { + return append(c.dynamicTargets(), c.staticTargets()...) +} + +// dynamicTargets returns all the supported targets +// that are retrieved at the interval specified +func (c *cmd) dynamicTargets() []string { + return []string{"metrics", "logs", "pprof"} +} + +// staticTargets returns all the supported targets +// that are retrieved at the start of the command execution +func (c *cmd) staticTargets() []string { + return []string{"host", "agent", "cluster"} +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return c.help +} + +const synopsis = "Records a debugging archive for operators" +const help = ` +Usage: consul debug [options] + + Monitors a Consul agent for the specified period of time, recording + information about the agent, cluster, and environment to an archive + written to the specified path. + + If ACLs are enabled, an 'operator:read' token must be supplied in order + to perform this operation. + + To create a debug archive in the current directory for the default + duration and interval, capturing all information available: + + $ consul debug + + The command stores captured data at the configured output path + through the duration, and will archive the data at the same + path if interrupted. + + Flags can be used to customize the duration and interval of the + operation. Duration is the total time to capture data for from the target + agent and interval controls how often dynamic data such as metrics + are scraped. + + $ consul debug -interval=20s -duration=1m + + The capture flag can be specified multiple times to limit information + retrieved. + + $ consul debug -capture metrics -capture agent + + By default, the archive containing the debugging information is + saved to the current directory as a .tar.gz file. The + output path can be specified, as well as an option to disable + archiving, leaving the directory intact. + + $ consul debug -output=/foo/bar/my-debugging -archive=false + + Note: Information collected by this command has the potential + to be highly sensitive. Sensitive material such as ACL tokens and + other commonly secret material are redacted automatically, but we + strongly recommend review of the data within the archive prior to + transmitting it. + + For a full list of options and examples, please see the Consul + documentation. +` diff --git a/command/debug/debug_test.go b/command/debug/debug_test.go new file mode 100644 index 000000000..071e9fbbc --- /dev/null +++ b/command/debug/debug_test.go @@ -0,0 +1,482 @@ +package debug + +import ( + "archive/tar" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/logger" + "github.com/hashicorp/consul/testrpc" + "github.com/hashicorp/consul/testutil" + "github.com/mitchellh/cli" +) + +func TestDebugCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi(), nil).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestDebugCommand(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t.Name(), ` + enable_debug = true + `) + a.Agent.LogWriter = logger.NewLogWriter(512) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui, nil) + cmd.validateTiming = false + + outputPath := fmt.Sprintf("%s/debug", testDir) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-output=" + outputPath, + "-duration=100ms", + "-interval=50ms", + } + + code := cmd.Run(args) + + if code != 0 { + t.Errorf("should exit 0, got code: %d", code) + } + + errOutput := ui.ErrorWriter.String() + if errOutput != "" { + t.Errorf("expected no error output, got %q", errOutput) + } +} + +func TestDebugCommand_Archive(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t.Name(), ` + enable_debug = true + `) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui, nil) + cmd.validateTiming = false + + outputPath := fmt.Sprintf("%s/debug", testDir) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-output=" + outputPath, + "-capture=agent", + } + + if code := cmd.Run(args); code != 0 { + t.Fatalf("should exit 0, got code: %d", code) + } + + archivePath := fmt.Sprintf("%s%s", outputPath, debugArchiveExtension) + file, err := os.Open(archivePath) + if err != nil { + t.Fatalf("failed to open archive: %s", err) + } + tr := tar.NewReader(file) + + for { + h, err := tr.Next() + + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("failed to read file in archive: %s", err) + } + + // ignore the outer directory + if h.Name == "debug" { + continue + } + + // should only contain this one capture target + if h.Name != "debug/agent.json" && h.Name != "debug/index.json" { + t.Fatalf("archive contents do not match: %s", h.Name) + } + } + +} + +func TestDebugCommand_ArgsBad(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + ui := cli.NewMockUi() + cmd := New(ui, nil) + + args := []string{ + "foo", + "bad", + } + + if code := cmd.Run(args); code == 0 { + t.Fatalf("should exit non-zero, got code: %d", code) + } + + errOutput := ui.ErrorWriter.String() + if !strings.Contains(errOutput, "Too many arguments") { + t.Errorf("expected error output, got %q", errOutput) + } +} + +func TestDebugCommand_OutputPathBad(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t.Name(), "") + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui, nil) + cmd.validateTiming = false + + outputPath := "" + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-output=" + outputPath, + "-duration=100ms", + "-interval=50ms", + } + + if code := cmd.Run(args); code == 0 { + t.Fatalf("should exit non-zero, got code: %d", code) + } + + errOutput := ui.ErrorWriter.String() + if !strings.Contains(errOutput, "no such file or directory") { + t.Errorf("expected error output, got %q", errOutput) + } +} + +func TestDebugCommand_OutputPathExists(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t.Name(), "") + a.Agent.LogWriter = logger.NewLogWriter(512) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui, nil) + cmd.validateTiming = false + + outputPath := fmt.Sprintf("%s/debug", testDir) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-output=" + outputPath, + "-duration=100ms", + "-interval=50ms", + } + + // Make a directory that conflicts with the output path + err := os.Mkdir(outputPath, 0755) + if err != nil { + t.Fatalf("duplicate test directory creation failed: %s", err) + } + + if code := cmd.Run(args); code == 0 { + t.Fatalf("should exit non-zero, got code: %d", code) + } + + errOutput := ui.ErrorWriter.String() + if !strings.Contains(errOutput, "directory already exists") { + t.Errorf("expected error output, got %q", errOutput) + } +} + +func TestDebugCommand_CaptureTargets(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + // used in -target param + targets []string + // existence verified after execution + files []string + // non-existence verified after execution + excludedFiles []string + }{ + "single": { + []string{"agent"}, + []string{"agent.json"}, + []string{"host.json", "cluster.json"}, + }, + "static": { + []string{"agent", "host", "cluster"}, + []string{"agent.json", "host.json", "cluster.json"}, + []string{"*/metrics.json"}, + }, + "metrics-only": { + []string{"metrics"}, + []string{"*/metrics.json"}, + []string{"agent.json", "host.json", "cluster.json"}, + }, + "all-but-pprof": { + []string{ + "metrics", + "logs", + "host", + "agent", + "cluster", + }, + []string{ + "host.json", + "agent.json", + "cluster.json", + "*/metrics.json", + "*/consul.log", + }, + []string{}, + }, + } + + for name, tc := range cases { + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t.Name(), ` + enable_debug = true + `) + a.Agent.LogWriter = logger.NewLogWriter(512) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui, nil) + cmd.validateTiming = false + + outputPath := fmt.Sprintf("%s/debug-%s", testDir, name) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-output=" + outputPath, + "-archive=false", + "-duration=100ms", + "-interval=50ms", + } + for _, t := range tc.targets { + args = append(args, "-capture="+t) + } + + if code := cmd.Run(args); code != 0 { + t.Fatalf("should exit 0, got code: %d", code) + } + + errOutput := ui.ErrorWriter.String() + if errOutput != "" { + t.Errorf("expected no error output, got %q", errOutput) + } + + // Ensure the debug data was written + _, err := os.Stat(outputPath) + if err != nil { + t.Fatalf("output path should exist: %s", err) + } + + // Ensure the captured static files exist + for _, f := range tc.files { + path := fmt.Sprintf("%s/%s", outputPath, f) + // Glob ignores file system errors + fs, _ := filepath.Glob(path) + if len(fs) <= 0 { + t.Fatalf("%s: output data should exist for %s", name, f) + } + } + + // Ensure any excluded files do not exist + for _, f := range tc.excludedFiles { + path := fmt.Sprintf("%s/%s", outputPath, f) + // Glob ignores file system errors + fs, _ := filepath.Glob(path) + if len(fs) > 0 { + t.Fatalf("%s: output data should not exist for %s", name, f) + } + } + } +} + +func TestDebugCommand_ProfilesExist(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t.Name(), ` + enable_debug = true + `) + a.Agent.LogWriter = logger.NewLogWriter(512) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui, nil) + cmd.validateTiming = false + + outputPath := fmt.Sprintf("%s/debug", testDir) + println(outputPath) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-output=" + outputPath, + // CPU profile has a minimum of 1s + "-archive=false", + "-duration=1s", + "-interval=1s", + "-capture=pprof", + } + + if code := cmd.Run(args); code != 0 { + t.Fatalf("should exit 0, got code: %d", code) + } + + profiles := []string{"heap.prof", "profile.prof", "goroutine.prof", "trace.out"} + // Glob ignores file system errors + for _, v := range profiles { + fs, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v)) + if len(fs) == 0 { + t.Errorf("output data should exist for %s", v) + } + } + + errOutput := ui.ErrorWriter.String() + if errOutput != "" { + t.Errorf("expected no error output, got %s", errOutput) + } +} + +func TestDebugCommand_ValidateTiming(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + duration string + interval string + output string + code int + }{ + "both": { + "20ms", + "10ms", + "duration must be longer", + 1, + }, + "short interval": { + "10s", + "10ms", + "interval must be longer", + 1, + }, + "lower duration": { + "20s", + "30s", + "must be longer than interval", + 1, + }, + } + + for name, tc := range cases { + // Because we're only testng validation, we want to shut down + // the valid duration test to avoid hanging + shutdownCh := make(chan struct{}) + + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t.Name(), "") + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui, shutdownCh) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-duration=" + tc.duration, + "-interval=" + tc.interval, + "-capture=agent", + } + code := cmd.Run(args) + + if code != tc.code { + t.Errorf("%s: should exit %d, got code: %d", name, tc.code, code) + } + + errOutput := ui.ErrorWriter.String() + if !strings.Contains(errOutput, tc.output) { + t.Errorf("%s: expected error output '%s', got '%q'", name, tc.output, errOutput) + } + } +} + +func TestDebugCommand_DebugDisabled(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "debug") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t.Name(), ` + enable_debug = false + `) + a.Agent.LogWriter = logger.NewLogWriter(512) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui, nil) + cmd.validateTiming = false + + outputPath := fmt.Sprintf("%s/debug", testDir) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-output=" + outputPath, + "-archive=false", + // CPU profile has a minimum of 1s + "-duration=1s", + "-interval=1s", + } + + if code := cmd.Run(args); code != 0 { + t.Fatalf("should exit 0, got code: %d", code) + } + + profiles := []string{"heap.prof", "profile.prof", "goroutine.prof", "trace.out"} + // Glob ignores file system errors + for _, v := range profiles { + fs, _ := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, v)) + if len(fs) > 0 { + t.Errorf("output data should not exist for %s", v) + } + } + + errOutput := ui.ErrorWriter.String() + if !strings.Contains(errOutput, "Unable to capture pprof") { + t.Errorf("expected warn output, got %s", errOutput) + } +} diff --git a/testutil/server.go b/testutil/server.go index 7b4df2d2e..6146aa6f4 100644 --- a/testutil/server.go +++ b/testutil/server.go @@ -98,6 +98,7 @@ type TestServerConfig struct { VerifyOutgoing bool `json:"verify_outgoing,omitempty"` EnableScriptChecks bool `json:"enable_script_checks,omitempty"` Connect map[string]interface{} `json:"connect,omitempty"` + EnableDebug bool `json:"enable_debug,omitempty"` ReadyTimeout time.Duration `json:"-"` Stdout, Stderr io.Writer `json:"-"` Args []string `json:"-"` diff --git a/vendor/github.com/shirou/gopsutil/LICENSE b/vendor/github.com/shirou/gopsutil/LICENSE new file mode 100644 index 000000000..da71a5e72 --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/LICENSE @@ -0,0 +1,61 @@ +gopsutil is distributed under BSD license reproduced below. + +Copyright (c) 2014, WAKAYAMA Shirou +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the gopsutil authors nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------- +internal/common/binary.go in the gopsutil is copied and modifid from golang/encoding/binary.go. + + + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/vendor/github.com/shirou/gopsutil/disk/disk.go b/vendor/github.com/shirou/gopsutil/disk/disk.go new file mode 100644 index 000000000..a2c47204d --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk.go @@ -0,0 +1,64 @@ +package disk + +import ( + "encoding/json" + + "github.com/shirou/gopsutil/internal/common" +) + +var invoke common.Invoker + +func init() { + invoke = common.Invoke{} +} + +type UsageStat struct { + Path string `json:"path"` + Fstype string `json:"fstype"` + Total uint64 `json:"total"` + Free uint64 `json:"free"` + Used uint64 `json:"used"` + UsedPercent float64 `json:"usedPercent"` + InodesTotal uint64 `json:"inodesTotal"` + InodesUsed uint64 `json:"inodesUsed"` + InodesFree uint64 `json:"inodesFree"` + InodesUsedPercent float64 `json:"inodesUsedPercent"` +} + +type PartitionStat struct { + Device string `json:"device"` + Mountpoint string `json:"mountpoint"` + Fstype string `json:"fstype"` + Opts string `json:"opts"` +} + +type IOCountersStat struct { + ReadCount uint64 `json:"readCount"` + MergedReadCount uint64 `json:"mergedReadCount"` + WriteCount uint64 `json:"writeCount"` + MergedWriteCount uint64 `json:"mergedWriteCount"` + ReadBytes uint64 `json:"readBytes"` + WriteBytes uint64 `json:"writeBytes"` + ReadTime uint64 `json:"readTime"` + WriteTime uint64 `json:"writeTime"` + IopsInProgress uint64 `json:"iopsInProgress"` + IoTime uint64 `json:"ioTime"` + WeightedIO uint64 `json:"weightedIO"` + Name string `json:"name"` + SerialNumber string `json:"serialNumber"` +} + +func (d UsageStat) String() string { + s, _ := json.Marshal(d) + return string(s) +} + +func (d PartitionStat) String() string { + s, _ := json.Marshal(d) + return string(s) +} + +func (d IOCountersStat) String() string { + s, _ := json.Marshal(d) + return string(s) +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_darwin.go b/vendor/github.com/shirou/gopsutil/disk/disk_darwin.go new file mode 100644 index 000000000..1ccb33031 --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_darwin.go @@ -0,0 +1,111 @@ +// +build darwin + +package disk + +import ( + "path" + "syscall" + "unsafe" + + "github.com/shirou/gopsutil/internal/common" +) + +func Partitions(all bool) ([]PartitionStat, error) { + var ret []PartitionStat + + count, err := Getfsstat(nil, MntWait) + if err != nil { + return ret, err + } + fs := make([]Statfs_t, count) + _, err = Getfsstat(fs, MntWait) + for _, stat := range fs { + opts := "rw" + if stat.Flags&MntReadOnly != 0 { + opts = "ro" + } + if stat.Flags&MntSynchronous != 0 { + opts += ",sync" + } + if stat.Flags&MntNoExec != 0 { + opts += ",noexec" + } + if stat.Flags&MntNoSuid != 0 { + opts += ",nosuid" + } + if stat.Flags&MntUnion != 0 { + opts += ",union" + } + if stat.Flags&MntAsync != 0 { + opts += ",async" + } + if stat.Flags&MntSuidDir != 0 { + opts += ",suiddir" + } + if stat.Flags&MntSoftDep != 0 { + opts += ",softdep" + } + if stat.Flags&MntNoSymFollow != 0 { + opts += ",nosymfollow" + } + if stat.Flags&MntGEOMJournal != 0 { + opts += ",gjounalc" + } + if stat.Flags&MntMultilabel != 0 { + opts += ",multilabel" + } + if stat.Flags&MntACLs != 0 { + opts += ",acls" + } + if stat.Flags&MntNoATime != 0 { + opts += ",noattime" + } + if stat.Flags&MntClusterRead != 0 { + opts += ",nocluster" + } + if stat.Flags&MntClusterWrite != 0 { + opts += ",noclusterw" + } + if stat.Flags&MntNFS4ACLs != 0 { + opts += ",nfs4acls" + } + d := PartitionStat{ + Device: common.IntToString(stat.Mntfromname[:]), + Mountpoint: common.IntToString(stat.Mntonname[:]), + Fstype: common.IntToString(stat.Fstypename[:]), + Opts: opts, + } + if all == false { + if !path.IsAbs(d.Device) || !common.PathExists(d.Device) { + continue + } + } + + ret = append(ret, d) + } + + return ret, nil +} + +func IOCounters() (map[string]IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func Getfsstat(buf []Statfs_t, flags int) (n int, err error) { + var _p0 unsafe.Pointer + var bufsize uintptr + if len(buf) > 0 { + _p0 = unsafe.Pointer(&buf[0]) + bufsize = unsafe.Sizeof(Statfs_t{}) * uintptr(len(buf)) + } + r0, _, e1 := syscall.Syscall(SYS_GETFSSTAT64, uintptr(_p0), bufsize, uintptr(flags)) + n = int(r0) + if e1 != 0 { + err = e1 + } + return +} + +func getFsType(stat syscall.Statfs_t) string { + return common.IntToString(stat.Fstypename[:]) +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_darwin_amd64.go b/vendor/github.com/shirou/gopsutil/disk/disk_darwin_amd64.go new file mode 100644 index 000000000..f58e21312 --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_darwin_amd64.go @@ -0,0 +1,58 @@ +// +build darwin +// +build amd64 + +package disk + +const ( + MntWait = 1 + MfsNameLen = 15 /* length of fs type name, not inc. nul */ + MNameLen = 90 /* length of buffer for returned name */ + + MFSTYPENAMELEN = 16 /* length of fs type name including null */ + MAXPATHLEN = 1024 + MNAMELEN = MAXPATHLEN + + SYS_GETFSSTAT64 = 347 +) + +type Fsid struct{ val [2]int32 } /* file system id type */ +type uid_t int32 + +// sys/mount.h +const ( + MntReadOnly = 0x00000001 /* read only filesystem */ + MntSynchronous = 0x00000002 /* filesystem written synchronously */ + MntNoExec = 0x00000004 /* can't exec from filesystem */ + MntNoSuid = 0x00000008 /* don't honor setuid bits on fs */ + MntUnion = 0x00000020 /* union with underlying filesystem */ + MntAsync = 0x00000040 /* filesystem written asynchronously */ + MntSuidDir = 0x00100000 /* special handling of SUID on dirs */ + MntSoftDep = 0x00200000 /* soft updates being done */ + MntNoSymFollow = 0x00400000 /* do not follow symlinks */ + MntGEOMJournal = 0x02000000 /* GEOM journal support enabled */ + MntMultilabel = 0x04000000 /* MAC support for individual objects */ + MntACLs = 0x08000000 /* ACL support enabled */ + MntNoATime = 0x10000000 /* disable update of file access time */ + MntClusterRead = 0x40000000 /* disable cluster read */ + MntClusterWrite = 0x80000000 /* disable cluster write */ + MntNFS4ACLs = 0x00000010 +) + +type Statfs_t struct { + Bsize uint32 + Iosize int32 + Blocks uint64 + Bfree uint64 + Bavail uint64 + Files uint64 + Ffree uint64 + Fsid Fsid + Owner uint32 + Type uint32 + Flags uint32 + Fssubtype uint32 + Fstypename [16]int8 + Mntonname [1024]int8 + Mntfromname [1024]int8 + Reserved [8]uint32 +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_darwin_arm64.go b/vendor/github.com/shirou/gopsutil/disk/disk_darwin_arm64.go new file mode 100644 index 000000000..52bcf4c8a --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_darwin_arm64.go @@ -0,0 +1,58 @@ +// +build darwin +// +build arm64 + +package disk + +const ( + MntWait = 1 + MfsNameLen = 15 /* length of fs type name, not inc. nul */ + MNameLen = 90 /* length of buffer for returned name */ + + MFSTYPENAMELEN = 16 /* length of fs type name including null */ + MAXPATHLEN = 1024 + MNAMELEN = MAXPATHLEN + + SYS_GETFSSTAT64 = 347 +) + +type Fsid struct{ val [2]int32 } /* file system id type */ +type uid_t int32 + +// sys/mount.h +const ( + MntReadOnly = 0x00000001 /* read only filesystem */ + MntSynchronous = 0x00000002 /* filesystem written synchronously */ + MntNoExec = 0x00000004 /* can't exec from filesystem */ + MntNoSuid = 0x00000008 /* don't honor setuid bits on fs */ + MntUnion = 0x00000020 /* union with underlying filesystem */ + MntAsync = 0x00000040 /* filesystem written asynchronously */ + MntSuidDir = 0x00100000 /* special handling of SUID on dirs */ + MntSoftDep = 0x00200000 /* soft updates being done */ + MntNoSymFollow = 0x00400000 /* do not follow symlinks */ + MntGEOMJournal = 0x02000000 /* GEOM journal support enabled */ + MntMultilabel = 0x04000000 /* MAC support for individual objects */ + MntACLs = 0x08000000 /* ACL support enabled */ + MntNoATime = 0x10000000 /* disable update of file access time */ + MntClusterRead = 0x40000000 /* disable cluster read */ + MntClusterWrite = 0x80000000 /* disable cluster write */ + MntNFS4ACLs = 0x00000010 +) + +type Statfs_t struct { + Bsize uint32 + Iosize int32 + Blocks uint64 + Bfree uint64 + Bavail uint64 + Files uint64 + Ffree uint64 + Fsid Fsid + Owner uint32 + Type uint32 + Flags uint32 + Fssubtype uint32 + Fstypename [16]int8 + Mntonname [1024]int8 + Mntfromname [1024]int8 + Reserved [8]uint32 +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_fallback.go b/vendor/github.com/shirou/gopsutil/disk/disk_fallback.go new file mode 100644 index 000000000..6fb01a986 --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_fallback.go @@ -0,0 +1,17 @@ +// +build !darwin,!linux,!freebsd,!openbsd,!windows + +package disk + +import "github.com/shirou/gopsutil/internal/common" + +func IOCounters() (map[string]IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func Partitions(all bool) ([]PartitionStat, error) { + return []PartitionStat{}, common.ErrNotImplementedError +} + +func Usage(path string) (*UsageStat, error) { + return nil, common.ErrNotImplementedError +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_freebsd.go b/vendor/github.com/shirou/gopsutil/disk/disk_freebsd.go new file mode 100644 index 000000000..6e76f31f2 --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_freebsd.go @@ -0,0 +1,176 @@ +// +build freebsd + +package disk + +import ( + "bytes" + "encoding/binary" + "path" + "strconv" + "syscall" + "unsafe" + + "github.com/shirou/gopsutil/internal/common" +) + +func Partitions(all bool) ([]PartitionStat, error) { + var ret []PartitionStat + + // get length + count, err := syscall.Getfsstat(nil, MNT_WAIT) + if err != nil { + return ret, err + } + + fs := make([]Statfs, count) + _, err = Getfsstat(fs, MNT_WAIT) + + for _, stat := range fs { + opts := "rw" + if stat.Flags&MNT_RDONLY != 0 { + opts = "ro" + } + if stat.Flags&MNT_SYNCHRONOUS != 0 { + opts += ",sync" + } + if stat.Flags&MNT_NOEXEC != 0 { + opts += ",noexec" + } + if stat.Flags&MNT_NOSUID != 0 { + opts += ",nosuid" + } + if stat.Flags&MNT_UNION != 0 { + opts += ",union" + } + if stat.Flags&MNT_ASYNC != 0 { + opts += ",async" + } + if stat.Flags&MNT_SUIDDIR != 0 { + opts += ",suiddir" + } + if stat.Flags&MNT_SOFTDEP != 0 { + opts += ",softdep" + } + if stat.Flags&MNT_NOSYMFOLLOW != 0 { + opts += ",nosymfollow" + } + if stat.Flags&MNT_GJOURNAL != 0 { + opts += ",gjounalc" + } + if stat.Flags&MNT_MULTILABEL != 0 { + opts += ",multilabel" + } + if stat.Flags&MNT_ACLS != 0 { + opts += ",acls" + } + if stat.Flags&MNT_NOATIME != 0 { + opts += ",noattime" + } + if stat.Flags&MNT_NOCLUSTERR != 0 { + opts += ",nocluster" + } + if stat.Flags&MNT_NOCLUSTERW != 0 { + opts += ",noclusterw" + } + if stat.Flags&MNT_NFS4ACLS != 0 { + opts += ",nfs4acls" + } + + d := PartitionStat{ + Device: common.IntToString(stat.Mntfromname[:]), + Mountpoint: common.IntToString(stat.Mntonname[:]), + Fstype: common.IntToString(stat.Fstypename[:]), + Opts: opts, + } + if all == false { + if !path.IsAbs(d.Device) || !common.PathExists(d.Device) { + continue + } + } + + ret = append(ret, d) + } + + return ret, nil +} + +func IOCounters() (map[string]IOCountersStat, error) { + // statinfo->devinfo->devstat + // /usr/include/devinfo.h + ret := make(map[string]IOCountersStat) + + r, err := syscall.Sysctl("kern.devstat.all") + if err != nil { + return nil, err + } + buf := []byte(r) + length := len(buf) + + count := int(uint64(length) / uint64(sizeOfDevstat)) + + buf = buf[8:] // devstat.all has version in the head. + // parse buf to Devstat + for i := 0; i < count; i++ { + b := buf[i*sizeOfDevstat : i*sizeOfDevstat+sizeOfDevstat] + d, err := parseDevstat(b) + if err != nil { + continue + } + un := strconv.Itoa(int(d.Unit_number)) + name := common.IntToString(d.Device_name[:]) + un + + ds := IOCountersStat{ + ReadCount: d.Operations[DEVSTAT_READ], + WriteCount: d.Operations[DEVSTAT_WRITE], + ReadBytes: d.Bytes[DEVSTAT_READ], + WriteBytes: d.Bytes[DEVSTAT_WRITE], + ReadTime: uint64(d.Duration[DEVSTAT_READ].Compute() * 1000), + WriteTime: uint64(d.Duration[DEVSTAT_WRITE].Compute() * 1000), + IoTime: uint64(d.Busy_time.Compute() * 1000), + Name: name, + } + ret[name] = ds + } + + return ret, nil +} + +func (b Bintime) Compute() float64 { + BINTIME_SCALE := 5.42101086242752217003726400434970855712890625e-20 + return float64(b.Sec) + float64(b.Frac)*BINTIME_SCALE +} + +// BT2LD(time) ((long double)(time).sec + (time).frac * BINTIME_SCALE) + +// Getfsstat is borrowed from pkg/syscall/syscall_freebsd.go +// change Statfs_t to Statfs in order to get more information +func Getfsstat(buf []Statfs, flags int) (n int, err error) { + var _p0 unsafe.Pointer + var bufsize uintptr + if len(buf) > 0 { + _p0 = unsafe.Pointer(&buf[0]) + bufsize = unsafe.Sizeof(Statfs{}) * uintptr(len(buf)) + } + r0, _, e1 := syscall.Syscall(syscall.SYS_GETFSSTAT, uintptr(_p0), bufsize, uintptr(flags)) + n = int(r0) + if e1 != 0 { + err = e1 + } + return +} + +func parseDevstat(buf []byte) (Devstat, error) { + var ds Devstat + br := bytes.NewReader(buf) + // err := binary.Read(br, binary.LittleEndian, &ds) + err := common.Read(br, binary.LittleEndian, &ds) + if err != nil { + return ds, err + } + + return ds, nil +} + +func getFsType(stat syscall.Statfs_t) string { + return common.IntToString(stat.Fstypename[:]) +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_freebsd_386.go b/vendor/github.com/shirou/gopsutil/disk/disk_freebsd_386.go new file mode 100644 index 000000000..0b3f536c8 --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_freebsd_386.go @@ -0,0 +1,112 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package disk + +const ( + sizeofPtr = 0x4 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x4 + sizeofLongLong = 0x8 + sizeofLongDouble = 0x8 + + DEVSTAT_NO_DATA = 0x00 + DEVSTAT_READ = 0x01 + DEVSTAT_WRITE = 0x02 + DEVSTAT_FREE = 0x03 + + MNT_RDONLY = 0x00000001 + MNT_SYNCHRONOUS = 0x00000002 + MNT_NOEXEC = 0x00000004 + MNT_NOSUID = 0x00000008 + MNT_UNION = 0x00000020 + MNT_ASYNC = 0x00000040 + MNT_SUIDDIR = 0x00100000 + MNT_SOFTDEP = 0x00200000 + MNT_NOSYMFOLLOW = 0x00400000 + MNT_GJOURNAL = 0x02000000 + MNT_MULTILABEL = 0x04000000 + MNT_ACLS = 0x08000000 + MNT_NOATIME = 0x10000000 + MNT_NOCLUSTERR = 0x40000000 + MNT_NOCLUSTERW = 0x80000000 + MNT_NFS4ACLS = 0x00000010 + + MNT_WAIT = 1 + MNT_NOWAIT = 2 + MNT_LAZY = 3 + MNT_SUSPEND = 4 +) + +const ( + sizeOfDevstat = 0xf0 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int32 + _C_long_long int64 + _C_long_double int64 +) + +type Statfs struct { + Version uint32 + Type uint32 + Flags uint64 + Bsize uint64 + Iosize uint64 + Blocks uint64 + Bfree uint64 + Bavail int64 + Files uint64 + Ffree int64 + Syncwrites uint64 + Asyncwrites uint64 + Syncreads uint64 + Asyncreads uint64 + Spare [10]uint64 + Namemax uint32 + Owner uint32 + Fsid Fsid + Charspare [80]int8 + Fstypename [16]int8 + Mntfromname [88]int8 + Mntonname [88]int8 +} +type Fsid struct { + Val [2]int32 +} + +type Devstat struct { + Sequence0 uint32 + Allocated int32 + Start_count uint32 + End_count uint32 + Busy_from Bintime + Dev_links _Ctype_struct___0 + Device_number uint32 + Device_name [16]int8 + Unit_number int32 + Bytes [4]uint64 + Operations [4]uint64 + Duration [4]Bintime + Busy_time Bintime + Creation_time Bintime + Block_size uint32 + Tag_types [3]uint64 + Flags uint32 + Device_type uint32 + Priority uint32 + Id *byte + Sequence1 uint32 +} +type Bintime struct { + Sec int32 + Frac uint64 +} + +type _Ctype_struct___0 struct { + Empty uint32 +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_freebsd_amd64.go b/vendor/github.com/shirou/gopsutil/disk/disk_freebsd_amd64.go new file mode 100644 index 000000000..89b617c9c --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_freebsd_amd64.go @@ -0,0 +1,115 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package disk + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 + sizeofLongDouble = 0x8 + + DEVSTAT_NO_DATA = 0x00 + DEVSTAT_READ = 0x01 + DEVSTAT_WRITE = 0x02 + DEVSTAT_FREE = 0x03 + + MNT_RDONLY = 0x00000001 + MNT_SYNCHRONOUS = 0x00000002 + MNT_NOEXEC = 0x00000004 + MNT_NOSUID = 0x00000008 + MNT_UNION = 0x00000020 + MNT_ASYNC = 0x00000040 + MNT_SUIDDIR = 0x00100000 + MNT_SOFTDEP = 0x00200000 + MNT_NOSYMFOLLOW = 0x00400000 + MNT_GJOURNAL = 0x02000000 + MNT_MULTILABEL = 0x04000000 + MNT_ACLS = 0x08000000 + MNT_NOATIME = 0x10000000 + MNT_NOCLUSTERR = 0x40000000 + MNT_NOCLUSTERW = 0x80000000 + MNT_NFS4ACLS = 0x00000010 + + MNT_WAIT = 1 + MNT_NOWAIT = 2 + MNT_LAZY = 3 + MNT_SUSPEND = 4 +) + +const ( + sizeOfDevstat = 0x120 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 + _C_long_double int64 +) + +type Statfs struct { + Version uint32 + Type uint32 + Flags uint64 + Bsize uint64 + Iosize uint64 + Blocks uint64 + Bfree uint64 + Bavail int64 + Files uint64 + Ffree int64 + Syncwrites uint64 + Asyncwrites uint64 + Syncreads uint64 + Asyncreads uint64 + Spare [10]uint64 + Namemax uint32 + Owner uint32 + Fsid Fsid + Charspare [80]int8 + Fstypename [16]int8 + Mntfromname [88]int8 + Mntonname [88]int8 +} +type Fsid struct { + Val [2]int32 +} + +type Devstat struct { + Sequence0 uint32 + Allocated int32 + Start_count uint32 + End_count uint32 + Busy_from Bintime + Dev_links _Ctype_struct___0 + Device_number uint32 + Device_name [16]int8 + Unit_number int32 + Bytes [4]uint64 + Operations [4]uint64 + Duration [4]Bintime + Busy_time Bintime + Creation_time Bintime + Block_size uint32 + Pad_cgo_0 [4]byte + Tag_types [3]uint64 + Flags uint32 + Device_type uint32 + Priority uint32 + Pad_cgo_1 [4]byte + ID *byte + Sequence1 uint32 + Pad_cgo_2 [4]byte +} +type Bintime struct { + Sec int64 + Frac uint64 +} + +type _Ctype_struct___0 struct { + Empty uint64 +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_linux.go b/vendor/github.com/shirou/gopsutil/disk/disk_linux.go new file mode 100644 index 000000000..51f17cd41 --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_linux.go @@ -0,0 +1,393 @@ +// +build linux + +package disk + +import ( + "fmt" + "os/exec" + "strconv" + "strings" + "syscall" + + "github.com/shirou/gopsutil/internal/common" +) + +const ( + SectorSize = 512 +) +const ( + // man statfs + ADFS_SUPER_MAGIC = 0xadf5 + AFFS_SUPER_MAGIC = 0xADFF + BDEVFS_MAGIC = 0x62646576 + BEFS_SUPER_MAGIC = 0x42465331 + BFS_MAGIC = 0x1BADFACE + BINFMTFS_MAGIC = 0x42494e4d + BTRFS_SUPER_MAGIC = 0x9123683E + CGROUP_SUPER_MAGIC = 0x27e0eb + CIFS_MAGIC_NUMBER = 0xFF534D42 + CODA_SUPER_MAGIC = 0x73757245 + COH_SUPER_MAGIC = 0x012FF7B7 + CRAMFS_MAGIC = 0x28cd3d45 + DEBUGFS_MAGIC = 0x64626720 + DEVFS_SUPER_MAGIC = 0x1373 + DEVPTS_SUPER_MAGIC = 0x1cd1 + EFIVARFS_MAGIC = 0xde5e81e4 + EFS_SUPER_MAGIC = 0x00414A53 + EXT_SUPER_MAGIC = 0x137D + EXT2_OLD_SUPER_MAGIC = 0xEF51 + EXT2_SUPER_MAGIC = 0xEF53 + EXT3_SUPER_MAGIC = 0xEF53 + EXT4_SUPER_MAGIC = 0xEF53 + FUSE_SUPER_MAGIC = 0x65735546 + FUTEXFS_SUPER_MAGIC = 0xBAD1DEA + HFS_SUPER_MAGIC = 0x4244 + HOSTFS_SUPER_MAGIC = 0x00c0ffee + HPFS_SUPER_MAGIC = 0xF995E849 + HUGETLBFS_MAGIC = 0x958458f6 + ISOFS_SUPER_MAGIC = 0x9660 + JFFS2_SUPER_MAGIC = 0x72b6 + JFS_SUPER_MAGIC = 0x3153464a + MINIX_SUPER_MAGIC = 0x137F /* orig. minix */ + MINIX_SUPER_MAGIC2 = 0x138F /* 30 char minix */ + MINIX2_SUPER_MAGIC = 0x2468 /* minix V2 */ + MINIX2_SUPER_MAGIC2 = 0x2478 /* minix V2, 30 char names */ + MINIX3_SUPER_MAGIC = 0x4d5a /* minix V3 fs, 60 char names */ + MQUEUE_MAGIC = 0x19800202 + MSDOS_SUPER_MAGIC = 0x4d44 + NCP_SUPER_MAGIC = 0x564c + NFS_SUPER_MAGIC = 0x6969 + NILFS_SUPER_MAGIC = 0x3434 + NTFS_SB_MAGIC = 0x5346544e + OCFS2_SUPER_MAGIC = 0x7461636f + OPENPROM_SUPER_MAGIC = 0x9fa1 + PIPEFS_MAGIC = 0x50495045 + PROC_SUPER_MAGIC = 0x9fa0 + PSTOREFS_MAGIC = 0x6165676C + QNX4_SUPER_MAGIC = 0x002f + QNX6_SUPER_MAGIC = 0x68191122 + RAMFS_MAGIC = 0x858458f6 + REISERFS_SUPER_MAGIC = 0x52654973 + ROMFS_MAGIC = 0x7275 + SELINUX_MAGIC = 0xf97cff8c + SMACK_MAGIC = 0x43415d53 + SMB_SUPER_MAGIC = 0x517B + SOCKFS_MAGIC = 0x534F434B + SQUASHFS_MAGIC = 0x73717368 + SYSFS_MAGIC = 0x62656572 + SYSV2_SUPER_MAGIC = 0x012FF7B6 + SYSV4_SUPER_MAGIC = 0x012FF7B5 + TMPFS_MAGIC = 0x01021994 + UDF_SUPER_MAGIC = 0x15013346 + UFS_MAGIC = 0x00011954 + USBDEVICE_SUPER_MAGIC = 0x9fa2 + V9FS_MAGIC = 0x01021997 + VXFS_SUPER_MAGIC = 0xa501FCF5 + XENFS_SUPER_MAGIC = 0xabba1974 + XENIX_SUPER_MAGIC = 0x012FF7B4 + XFS_SUPER_MAGIC = 0x58465342 + _XIAFS_SUPER_MAGIC = 0x012FD16D + + AFS_SUPER_MAGIC = 0x5346414F + AUFS_SUPER_MAGIC = 0x61756673 + ANON_INODE_FS_SUPER_MAGIC = 0x09041934 + CEPH_SUPER_MAGIC = 0x00C36400 + ECRYPTFS_SUPER_MAGIC = 0xF15F + FAT_SUPER_MAGIC = 0x4006 + FHGFS_SUPER_MAGIC = 0x19830326 + FUSEBLK_SUPER_MAGIC = 0x65735546 + FUSECTL_SUPER_MAGIC = 0x65735543 + GFS_SUPER_MAGIC = 0x1161970 + GPFS_SUPER_MAGIC = 0x47504653 + MTD_INODE_FS_SUPER_MAGIC = 0x11307854 + INOTIFYFS_SUPER_MAGIC = 0x2BAD1DEA + ISOFS_R_WIN_SUPER_MAGIC = 0x4004 + ISOFS_WIN_SUPER_MAGIC = 0x4000 + JFFS_SUPER_MAGIC = 0x07C0 + KAFS_SUPER_MAGIC = 0x6B414653 + LUSTRE_SUPER_MAGIC = 0x0BD00BD0 + NFSD_SUPER_MAGIC = 0x6E667364 + PANFS_SUPER_MAGIC = 0xAAD7AAEA + RPC_PIPEFS_SUPER_MAGIC = 0x67596969 + SECURITYFS_SUPER_MAGIC = 0x73636673 + UFS_BYTESWAPPED_SUPER_MAGIC = 0x54190100 + VMHGFS_SUPER_MAGIC = 0xBACBACBC + VZFS_SUPER_MAGIC = 0x565A4653 + ZFS_SUPER_MAGIC = 0x2FC12FC1 +) + +// coreutils/src/stat.c +var fsTypeMap = map[int64]string{ + ADFS_SUPER_MAGIC: "adfs", /* 0xADF5 local */ + AFFS_SUPER_MAGIC: "affs", /* 0xADFF local */ + AFS_SUPER_MAGIC: "afs", /* 0x5346414F remote */ + ANON_INODE_FS_SUPER_MAGIC: "anon-inode FS", /* 0x09041934 local */ + AUFS_SUPER_MAGIC: "aufs", /* 0x61756673 remote */ + // AUTOFS_SUPER_MAGIC: "autofs", /* 0x0187 local */ + BEFS_SUPER_MAGIC: "befs", /* 0x42465331 local */ + BDEVFS_MAGIC: "bdevfs", /* 0x62646576 local */ + BFS_MAGIC: "bfs", /* 0x1BADFACE local */ + BINFMTFS_MAGIC: "binfmt_misc", /* 0x42494E4D local */ + BTRFS_SUPER_MAGIC: "btrfs", /* 0x9123683E local */ + CEPH_SUPER_MAGIC: "ceph", /* 0x00C36400 remote */ + CGROUP_SUPER_MAGIC: "cgroupfs", /* 0x0027E0EB local */ + CIFS_MAGIC_NUMBER: "cifs", /* 0xFF534D42 remote */ + CODA_SUPER_MAGIC: "coda", /* 0x73757245 remote */ + COH_SUPER_MAGIC: "coh", /* 0x012FF7B7 local */ + CRAMFS_MAGIC: "cramfs", /* 0x28CD3D45 local */ + DEBUGFS_MAGIC: "debugfs", /* 0x64626720 local */ + DEVFS_SUPER_MAGIC: "devfs", /* 0x1373 local */ + DEVPTS_SUPER_MAGIC: "devpts", /* 0x1CD1 local */ + ECRYPTFS_SUPER_MAGIC: "ecryptfs", /* 0xF15F local */ + EFS_SUPER_MAGIC: "efs", /* 0x00414A53 local */ + EXT_SUPER_MAGIC: "ext", /* 0x137D local */ + EXT2_SUPER_MAGIC: "ext2/ext3", /* 0xEF53 local */ + EXT2_OLD_SUPER_MAGIC: "ext2", /* 0xEF51 local */ + FAT_SUPER_MAGIC: "fat", /* 0x4006 local */ + FHGFS_SUPER_MAGIC: "fhgfs", /* 0x19830326 remote */ + FUSEBLK_SUPER_MAGIC: "fuseblk", /* 0x65735546 remote */ + FUSECTL_SUPER_MAGIC: "fusectl", /* 0x65735543 remote */ + FUTEXFS_SUPER_MAGIC: "futexfs", /* 0x0BAD1DEA local */ + GFS_SUPER_MAGIC: "gfs/gfs2", /* 0x1161970 remote */ + GPFS_SUPER_MAGIC: "gpfs", /* 0x47504653 remote */ + HFS_SUPER_MAGIC: "hfs", /* 0x4244 local */ + HPFS_SUPER_MAGIC: "hpfs", /* 0xF995E849 local */ + HUGETLBFS_MAGIC: "hugetlbfs", /* 0x958458F6 local */ + MTD_INODE_FS_SUPER_MAGIC: "inodefs", /* 0x11307854 local */ + INOTIFYFS_SUPER_MAGIC: "inotifyfs", /* 0x2BAD1DEA local */ + ISOFS_SUPER_MAGIC: "isofs", /* 0x9660 local */ + ISOFS_R_WIN_SUPER_MAGIC: "isofs", /* 0x4004 local */ + ISOFS_WIN_SUPER_MAGIC: "isofs", /* 0x4000 local */ + JFFS_SUPER_MAGIC: "jffs", /* 0x07C0 local */ + JFFS2_SUPER_MAGIC: "jffs2", /* 0x72B6 local */ + JFS_SUPER_MAGIC: "jfs", /* 0x3153464A local */ + KAFS_SUPER_MAGIC: "k-afs", /* 0x6B414653 remote */ + LUSTRE_SUPER_MAGIC: "lustre", /* 0x0BD00BD0 remote */ + MINIX_SUPER_MAGIC: "minix", /* 0x137F local */ + MINIX_SUPER_MAGIC2: "minix (30 char.)", /* 0x138F local */ + MINIX2_SUPER_MAGIC: "minix v2", /* 0x2468 local */ + MINIX2_SUPER_MAGIC2: "minix v2 (30 char.)", /* 0x2478 local */ + MINIX3_SUPER_MAGIC: "minix3", /* 0x4D5A local */ + MQUEUE_MAGIC: "mqueue", /* 0x19800202 local */ + MSDOS_SUPER_MAGIC: "msdos", /* 0x4D44 local */ + NCP_SUPER_MAGIC: "novell", /* 0x564C remote */ + NFS_SUPER_MAGIC: "nfs", /* 0x6969 remote */ + NFSD_SUPER_MAGIC: "nfsd", /* 0x6E667364 remote */ + NILFS_SUPER_MAGIC: "nilfs", /* 0x3434 local */ + NTFS_SB_MAGIC: "ntfs", /* 0x5346544E local */ + OPENPROM_SUPER_MAGIC: "openprom", /* 0x9FA1 local */ + OCFS2_SUPER_MAGIC: "ocfs2", /* 0x7461636f remote */ + PANFS_SUPER_MAGIC: "panfs", /* 0xAAD7AAEA remote */ + PIPEFS_MAGIC: "pipefs", /* 0x50495045 remote */ + PROC_SUPER_MAGIC: "proc", /* 0x9FA0 local */ + PSTOREFS_MAGIC: "pstorefs", /* 0x6165676C local */ + QNX4_SUPER_MAGIC: "qnx4", /* 0x002F local */ + QNX6_SUPER_MAGIC: "qnx6", /* 0x68191122 local */ + RAMFS_MAGIC: "ramfs", /* 0x858458F6 local */ + REISERFS_SUPER_MAGIC: "reiserfs", /* 0x52654973 local */ + ROMFS_MAGIC: "romfs", /* 0x7275 local */ + RPC_PIPEFS_SUPER_MAGIC: "rpc_pipefs", /* 0x67596969 local */ + SECURITYFS_SUPER_MAGIC: "securityfs", /* 0x73636673 local */ + SELINUX_MAGIC: "selinux", /* 0xF97CFF8C local */ + SMB_SUPER_MAGIC: "smb", /* 0x517B remote */ + SOCKFS_MAGIC: "sockfs", /* 0x534F434B local */ + SQUASHFS_MAGIC: "squashfs", /* 0x73717368 local */ + SYSFS_MAGIC: "sysfs", /* 0x62656572 local */ + SYSV2_SUPER_MAGIC: "sysv2", /* 0x012FF7B6 local */ + SYSV4_SUPER_MAGIC: "sysv4", /* 0x012FF7B5 local */ + TMPFS_MAGIC: "tmpfs", /* 0x01021994 local */ + UDF_SUPER_MAGIC: "udf", /* 0x15013346 local */ + UFS_MAGIC: "ufs", /* 0x00011954 local */ + UFS_BYTESWAPPED_SUPER_MAGIC: "ufs", /* 0x54190100 local */ + USBDEVICE_SUPER_MAGIC: "usbdevfs", /* 0x9FA2 local */ + V9FS_MAGIC: "v9fs", /* 0x01021997 local */ + VMHGFS_SUPER_MAGIC: "vmhgfs", /* 0xBACBACBC remote */ + VXFS_SUPER_MAGIC: "vxfs", /* 0xA501FCF5 local */ + VZFS_SUPER_MAGIC: "vzfs", /* 0x565A4653 local */ + XENFS_SUPER_MAGIC: "xenfs", /* 0xABBA1974 local */ + XENIX_SUPER_MAGIC: "xenix", /* 0x012FF7B4 local */ + XFS_SUPER_MAGIC: "xfs", /* 0x58465342 local */ + _XIAFS_SUPER_MAGIC: "xia", /* 0x012FD16D local */ + ZFS_SUPER_MAGIC: "zfs", /* 0x2FC12FC1 local */ +} + +// Partitions returns disk partitions. If all is false, returns +// physical devices only (e.g. hard disks, cd-rom drives, USB keys) +// and ignore all others (e.g. memory partitions such as /dev/shm) +// +// should use setmntent(3) but this implement use /etc/mtab file +func Partitions(all bool) ([]PartitionStat, error) { + filename := common.HostEtc("mtab") + lines, err := common.ReadLines(filename) + if err != nil { + return nil, err + } + + fs, err := getFileSystems() + if err != nil { + return nil, err + } + + ret := make([]PartitionStat, 0, len(lines)) + + for _, line := range lines { + fields := strings.Fields(line) + d := PartitionStat{ + Device: fields[0], + Mountpoint: fields[1], + Fstype: fields[2], + Opts: fields[3], + } + if all == false { + if d.Device == "none" || !common.StringsHas(fs, d.Fstype) { + continue + } + } + ret = append(ret, d) + } + + return ret, nil +} + +// getFileSystems returns supported filesystems from /proc/filesystems +func getFileSystems() ([]string, error) { + filename := common.HostProc("filesystems") + lines, err := common.ReadLines(filename) + if err != nil { + return nil, err + } + var ret []string + for _, line := range lines { + if !strings.HasPrefix(line, "nodev") { + ret = append(ret, strings.TrimSpace(line)) + continue + } + t := strings.Split(line, "\t") + if len(t) != 2 || t[1] != "zfs" { + continue + } + ret = append(ret, strings.TrimSpace(t[1])) + } + + return ret, nil +} + +func IOCounters() (map[string]IOCountersStat, error) { + filename := common.HostProc("diskstats") + lines, err := common.ReadLines(filename) + if err != nil { + return nil, err + } + ret := make(map[string]IOCountersStat, 0) + empty := IOCountersStat{} + + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) < 14 { + // malformed line in /proc/diskstats, avoid panic by ignoring. + continue + } + name := fields[2] + reads, err := strconv.ParseUint((fields[3]), 10, 64) + if err != nil { + return ret, err + } + mergedReads, err := strconv.ParseUint((fields[4]), 10, 64) + if err != nil { + return ret, err + } + rbytes, err := strconv.ParseUint((fields[5]), 10, 64) + if err != nil { + return ret, err + } + rtime, err := strconv.ParseUint((fields[6]), 10, 64) + if err != nil { + return ret, err + } + writes, err := strconv.ParseUint((fields[7]), 10, 64) + if err != nil { + return ret, err + } + mergedWrites, err := strconv.ParseUint((fields[8]), 10, 64) + if err != nil { + return ret, err + } + wbytes, err := strconv.ParseUint((fields[9]), 10, 64) + if err != nil { + return ret, err + } + wtime, err := strconv.ParseUint((fields[10]), 10, 64) + if err != nil { + return ret, err + } + iopsInProgress, err := strconv.ParseUint((fields[11]), 10, 64) + if err != nil { + return ret, err + } + iotime, err := strconv.ParseUint((fields[12]), 10, 64) + if err != nil { + return ret, err + } + weightedIO, err := strconv.ParseUint((fields[13]), 10, 64) + if err != nil { + return ret, err + } + d := IOCountersStat{ + ReadBytes: rbytes * SectorSize, + WriteBytes: wbytes * SectorSize, + ReadCount: reads, + WriteCount: writes, + MergedReadCount: mergedReads, + MergedWriteCount: mergedWrites, + ReadTime: rtime, + WriteTime: wtime, + IopsInProgress: iopsInProgress, + IoTime: iotime, + WeightedIO: weightedIO, + } + if d == empty { + continue + } + d.Name = name + + d.SerialNumber = GetDiskSerialNumber(name) + ret[name] = d + } + return ret, nil +} + +// GetDiskSerialNumber returns Serial Number of given device or empty string +// on error. Name of device is expected, eg. /dev/sda +func GetDiskSerialNumber(name string) string { + n := fmt.Sprintf("--name=%s", name) + udevadm, err := exec.LookPath("/sbin/udevadm") + if err != nil { + return "" + } + + out, err := invoke.Command(udevadm, "info", "--query=property", n) + + // does not return error, just an empty string + if err != nil { + return "" + } + lines := strings.Split(string(out), "\n") + for _, line := range lines { + values := strings.Split(line, "=") + if len(values) < 2 || values[0] != "ID_SERIAL" { + // only get ID_SERIAL, not ID_SERIAL_SHORT + continue + } + return values[1] + } + return "" +} + +func getFsType(stat syscall.Statfs_t) string { + t := int64(stat.Type) + ret, ok := fsTypeMap[t] + if !ok { + return "" + } + return ret +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_openbsd.go b/vendor/github.com/shirou/gopsutil/disk/disk_openbsd.go new file mode 100644 index 000000000..2129b2b6a --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_openbsd.go @@ -0,0 +1,158 @@ +// +build openbsd + +package disk + +import ( + "bytes" + "encoding/binary" + "path" + "syscall" + "unsafe" + + "github.com/shirou/gopsutil/internal/common" +) + +func Partitions(all bool) ([]PartitionStat, error) { + var ret []PartitionStat + + // get length + count, err := syscall.Getfsstat(nil, MNT_WAIT) + if err != nil { + return ret, err + } + + fs := make([]Statfs, count) + _, err = Getfsstat(fs, MNT_WAIT) + + for _, stat := range fs { + opts := "rw" + if stat.F_flags&MNT_RDONLY != 0 { + opts = "ro" + } + if stat.F_flags&MNT_SYNCHRONOUS != 0 { + opts += ",sync" + } + if stat.F_flags&MNT_NOEXEC != 0 { + opts += ",noexec" + } + if stat.F_flags&MNT_NOSUID != 0 { + opts += ",nosuid" + } + if stat.F_flags&MNT_NODEV != 0 { + opts += ",nodev" + } + if stat.F_flags&MNT_ASYNC != 0 { + opts += ",async" + } + + d := PartitionStat{ + Device: common.IntToString(stat.F_mntfromname[:]), + Mountpoint: common.IntToString(stat.F_mntonname[:]), + Fstype: common.IntToString(stat.F_fstypename[:]), + Opts: opts, + } + if all == false { + if !path.IsAbs(d.Device) || !common.PathExists(d.Device) { + continue + } + } + + ret = append(ret, d) + } + + return ret, nil +} + +func IOCounters() (map[string]IOCountersStat, error) { + ret := make(map[string]IOCountersStat) + + r, err := syscall.Sysctl("hw.diskstats") + if err != nil { + return nil, err + } + buf := []byte(r) + length := len(buf) + + count := int(uint64(length) / uint64(sizeOfDiskstats)) + + // parse buf to Diskstats + for i := 0; i < count; i++ { + b := buf[i*sizeOfDiskstats : i*sizeOfDiskstats+sizeOfDiskstats] + d, err := parseDiskstats(b) + if err != nil { + continue + } + name := common.IntToString(d.Name[:]) + + ds := IOCountersStat{ + ReadCount: d.Rxfer, + WriteCount: d.Wxfer, + ReadBytes: d.Rbytes, + WriteBytes: d.Wbytes, + Name: name, + } + ret[name] = ds + } + + return ret, nil +} + +// BT2LD(time) ((long double)(time).sec + (time).frac * BINTIME_SCALE) + +// Getfsstat is borrowed from pkg/syscall/syscall_freebsd.go +// change Statfs_t to Statfs in order to get more information +func Getfsstat(buf []Statfs, flags int) (n int, err error) { + var _p0 unsafe.Pointer + var bufsize uintptr + if len(buf) > 0 { + _p0 = unsafe.Pointer(&buf[0]) + bufsize = unsafe.Sizeof(Statfs{}) * uintptr(len(buf)) + } + r0, _, e1 := syscall.Syscall(syscall.SYS_GETFSSTAT, uintptr(_p0), bufsize, uintptr(flags)) + n = int(r0) + if e1 != 0 { + err = e1 + } + return +} + +func parseDiskstats(buf []byte) (Diskstats, error) { + var ds Diskstats + br := bytes.NewReader(buf) + // err := binary.Read(br, binary.LittleEndian, &ds) + err := common.Read(br, binary.LittleEndian, &ds) + if err != nil { + return ds, err + } + + return ds, nil +} + +func Usage(path string) (*UsageStat, error) { + stat := syscall.Statfs_t{} + err := syscall.Statfs(path, &stat) + if err != nil { + return nil, err + } + bsize := stat.F_bsize + + ret := &UsageStat{ + Path: path, + Fstype: getFsType(stat), + Total: (uint64(stat.F_blocks) * uint64(bsize)), + Free: (uint64(stat.F_bavail) * uint64(bsize)), + InodesTotal: (uint64(stat.F_files)), + InodesFree: (uint64(stat.F_ffree)), + } + + ret.InodesUsed = (ret.InodesTotal - ret.InodesFree) + ret.InodesUsedPercent = (float64(ret.InodesUsed) / float64(ret.InodesTotal)) * 100.0 + ret.Used = (uint64(stat.F_blocks) - uint64(stat.F_bfree)) * uint64(bsize) + ret.UsedPercent = (float64(ret.Used) / float64(ret.Total)) * 100.0 + + return ret, nil +} + +func getFsType(stat syscall.Statfs_t) string { + return common.IntToString(stat.F_fstypename[:]) +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_openbsd_amd64.go b/vendor/github.com/shirou/gopsutil/disk/disk_openbsd_amd64.go new file mode 100644 index 000000000..07a845fbc --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_openbsd_amd64.go @@ -0,0 +1,91 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package disk + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 + sizeofLongDouble = 0x8 + + DEVSTAT_NO_DATA = 0x00 + DEVSTAT_READ = 0x01 + DEVSTAT_WRITE = 0x02 + DEVSTAT_FREE = 0x03 + + MNT_RDONLY = 0x00000001 + MNT_SYNCHRONOUS = 0x00000002 + MNT_NOEXEC = 0x00000004 + MNT_NOSUID = 0x00000008 + MNT_NODEV = 0x00000010 + MNT_ASYNC = 0x00000040 + + MNT_WAIT = 1 + MNT_NOWAIT = 2 + MNT_LAZY = 3 +) + +const ( + sizeOfDiskstats = 0x70 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 + _C_long_double int64 +) + +type Statfs struct { + F_flags uint32 + F_bsize uint32 + F_iosize uint32 + Pad_cgo_0 [4]byte + F_blocks uint64 + F_bfree uint64 + F_bavail int64 + F_files uint64 + F_ffree uint64 + F_favail int64 + F_syncwrites uint64 + F_syncreads uint64 + F_asyncwrites uint64 + F_asyncreads uint64 + F_fsid Fsid + F_namemax uint32 + F_owner uint32 + F_ctime uint64 + F_fstypename [16]int8 + F_mntonname [90]int8 + F_mntfromname [90]int8 + F_mntfromspec [90]int8 + Pad_cgo_1 [2]byte + Mount_info [160]byte +} +type Diskstats struct { + Name [16]int8 + Busy int32 + Pad_cgo_0 [4]byte + Rxfer uint64 + Wxfer uint64 + Seek uint64 + Rbytes uint64 + Wbytes uint64 + Attachtime Timeval + Timestamp Timeval + Time Timeval +} +type Fsid struct { + Val [2]int32 +} +type Timeval struct { + Sec int64 + Usec int64 +} + +type Diskstat struct{} +type Bintime struct{} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_unix.go b/vendor/github.com/shirou/gopsutil/disk/disk_unix.go new file mode 100644 index 000000000..f0616c30a --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_unix.go @@ -0,0 +1,45 @@ +// +build freebsd linux darwin + +package disk + +import "syscall" + +func Usage(path string) (*UsageStat, error) { + stat := syscall.Statfs_t{} + err := syscall.Statfs(path, &stat) + if err != nil { + return nil, err + } + bsize := stat.Bsize + + ret := &UsageStat{ + Path: path, + Fstype: getFsType(stat), + Total: (uint64(stat.Blocks) * uint64(bsize)), + Free: (uint64(stat.Bavail) * uint64(bsize)), + InodesTotal: (uint64(stat.Files)), + InodesFree: (uint64(stat.Ffree)), + } + + // if could not get InodesTotal, return empty + if ret.InodesTotal < ret.InodesFree { + return ret, nil + } + + ret.InodesUsed = (ret.InodesTotal - ret.InodesFree) + ret.Used = (uint64(stat.Blocks) - uint64(stat.Bfree)) * uint64(bsize) + + if ret.InodesTotal == 0 { + ret.InodesUsedPercent = 0 + } else { + ret.InodesUsedPercent = (float64(ret.InodesUsed) / float64(ret.InodesTotal)) * 100.0 + } + + if ret.Total == 0 { + ret.UsedPercent = 0 + } else { + ret.UsedPercent = (float64(ret.Used) / float64(ret.Total)) * 100.0 + } + + return ret, nil +} diff --git a/vendor/github.com/shirou/gopsutil/disk/disk_windows.go b/vendor/github.com/shirou/gopsutil/disk/disk_windows.go new file mode 100644 index 000000000..b3a30d69f --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/disk/disk_windows.go @@ -0,0 +1,155 @@ +// +build windows + +package disk + +import ( + "bytes" + "syscall" + "unsafe" + + "github.com/StackExchange/wmi" + + "github.com/shirou/gopsutil/internal/common" +) + +var ( + procGetDiskFreeSpaceExW = common.Modkernel32.NewProc("GetDiskFreeSpaceExW") + procGetLogicalDriveStringsW = common.Modkernel32.NewProc("GetLogicalDriveStringsW") + procGetDriveType = common.Modkernel32.NewProc("GetDriveTypeW") + provGetVolumeInformation = common.Modkernel32.NewProc("GetVolumeInformationW") +) + +var ( + FileFileCompression = int64(16) // 0x00000010 + FileReadOnlyVolume = int64(524288) // 0x00080000 +) + +type Win32_PerfFormattedData struct { + Name string + AvgDiskBytesPerRead uint64 + AvgDiskBytesPerWrite uint64 + AvgDiskReadQueueLength uint64 + AvgDiskWriteQueueLength uint64 + AvgDisksecPerRead uint64 + AvgDisksecPerWrite uint64 +} + +const WaitMSec = 500 + +func Usage(path string) (*UsageStat, error) { + ret := &UsageStat{} + + lpFreeBytesAvailable := int64(0) + lpTotalNumberOfBytes := int64(0) + lpTotalNumberOfFreeBytes := int64(0) + diskret, _, err := procGetDiskFreeSpaceExW.Call( + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), + uintptr(unsafe.Pointer(&lpFreeBytesAvailable)), + uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)), + uintptr(unsafe.Pointer(&lpTotalNumberOfFreeBytes))) + if diskret == 0 { + return nil, err + } + ret = &UsageStat{ + Path: path, + Total: uint64(lpTotalNumberOfBytes), + Free: uint64(lpTotalNumberOfFreeBytes), + Used: uint64(lpTotalNumberOfBytes) - uint64(lpTotalNumberOfFreeBytes), + UsedPercent: (float64(lpTotalNumberOfBytes) - float64(lpTotalNumberOfFreeBytes)) / float64(lpTotalNumberOfBytes) * 100, + // InodesTotal: 0, + // InodesFree: 0, + // InodesUsed: 0, + // InodesUsedPercent: 0, + } + return ret, nil +} + +func Partitions(all bool) ([]PartitionStat, error) { + var ret []PartitionStat + lpBuffer := make([]byte, 254) + diskret, _, err := procGetLogicalDriveStringsW.Call( + uintptr(len(lpBuffer)), + uintptr(unsafe.Pointer(&lpBuffer[0]))) + if diskret == 0 { + return ret, err + } + for _, v := range lpBuffer { + if v >= 65 && v <= 90 { + path := string(v) + ":" + if path == "A:" || path == "B:" { // skip floppy drives + continue + } + typepath, _ := syscall.UTF16PtrFromString(path) + typeret, _, _ := procGetDriveType.Call(uintptr(unsafe.Pointer(typepath))) + if typeret == 0 { + return ret, syscall.GetLastError() + } + // 2: DRIVE_REMOVABLE 3: DRIVE_FIXED 5: DRIVE_CDROM + + if typeret == 2 || typeret == 3 || typeret == 5 { + lpVolumeNameBuffer := make([]byte, 256) + lpVolumeSerialNumber := int64(0) + lpMaximumComponentLength := int64(0) + lpFileSystemFlags := int64(0) + lpFileSystemNameBuffer := make([]byte, 256) + volpath, _ := syscall.UTF16PtrFromString(string(v) + ":/") + driveret, _, err := provGetVolumeInformation.Call( + uintptr(unsafe.Pointer(volpath)), + uintptr(unsafe.Pointer(&lpVolumeNameBuffer[0])), + uintptr(len(lpVolumeNameBuffer)), + uintptr(unsafe.Pointer(&lpVolumeSerialNumber)), + uintptr(unsafe.Pointer(&lpMaximumComponentLength)), + uintptr(unsafe.Pointer(&lpFileSystemFlags)), + uintptr(unsafe.Pointer(&lpFileSystemNameBuffer[0])), + uintptr(len(lpFileSystemNameBuffer))) + if driveret == 0 { + if typeret == 5 { + continue //device is not ready will happen if there is no disk in the drive + } + return ret, err + } + opts := "rw" + if lpFileSystemFlags&FileReadOnlyVolume != 0 { + opts = "ro" + } + if lpFileSystemFlags&FileFileCompression != 0 { + opts += ".compress" + } + + d := PartitionStat{ + Mountpoint: path, + Device: path, + Fstype: string(bytes.Replace(lpFileSystemNameBuffer, []byte("\x00"), []byte(""), -1)), + Opts: opts, + } + ret = append(ret, d) + } + } + } + return ret, nil +} + +func IOCounters() (map[string]IOCountersStat, error) { + ret := make(map[string]IOCountersStat, 0) + var dst []Win32_PerfFormattedData + + err := wmi.Query("SELECT * FROM Win32_PerfFormattedData_PerfDisk_LogicalDisk ", &dst) + if err != nil { + return ret, err + } + for _, d := range dst { + if len(d.Name) > 3 { // not get _Total or Harddrive + continue + } + ret[d.Name] = IOCountersStat{ + Name: d.Name, + ReadCount: uint64(d.AvgDiskReadQueueLength), + WriteCount: d.AvgDiskWriteQueueLength, + ReadBytes: uint64(d.AvgDiskBytesPerRead), + WriteBytes: uint64(d.AvgDiskBytesPerWrite), + ReadTime: d.AvgDisksecPerRead, + WriteTime: d.AvgDisksecPerWrite, + } + } + return ret, nil +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 24e946448..b56d5f7a6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -258,6 +258,7 @@ {"path":"github.com/ryanuber/go-glob","checksumSHA1":"6JP37UqrI0H80Gpk0Y2P+KXgn5M=","revision":"256dc444b735e061061cf46c809487313d5b0065","revisionTime":"2017-01-28T01:21:29Z"}, {"path":"github.com/sean-/seed","checksumSHA1":"A/YUMbGg1LHIeK2+NLZBt+MIAao=","revision":"3c72d44db0c567f7c901f9c5da5fe68392227750","revisionTime":"2017-02-08T16:47:21Z"}, {"path":"github.com/shirou/gopsutil/cpu","checksumSHA1":"zW2k8E1gkuySzTz2eXuSEDhpffY=","revision":"32b6636de04b303274daac3ca2b10d3b0e4afc35","revisionTime":"2017-02-04T05:36:48Z"}, + {"path":"github.com/shirou/gopsutil/disk","checksumSHA1":"wxkkOLGCVJ/qrh+eSSFyIW2kTd8=","revision":"b62e301a8b9958eebb7299683eb57fab229a9501","revisionTime":"2017-02-08T02:55:55Z"}, {"path":"github.com/shirou/gopsutil/host","checksumSHA1":"GsqEEmGv6sj8DreS2SYXRkoZ9NI=","revision":"b62e301a8b9958eebb7299683eb57fab229a9501","revisionTime":"2017-02-08T02:55:55Z"}, {"path":"github.com/shirou/gopsutil/internal/common","checksumSHA1":"hz9RxkaV3Tnju2eiHBWO/Yv7n5c=","revision":"32b6636de04b303274daac3ca2b10d3b0e4afc35","revisionTime":"2017-02-04T05:36:48Z"}, {"path":"github.com/shirou/gopsutil/mem","checksumSHA1":"XQwjGKI51Y3aQ3/jNyRh9Gnprgg=","revision":"32b6636de04b303274daac3ca2b10d3b0e4afc35","revisionTime":"2017-02-04T05:36:48Z"}, diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md index b8b501d65..4ec9af687 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -936,7 +936,8 @@ default will automatically work with some tooling. be checked using the agent's credentials. This was added in Consul 1.0.1 and defaults to false. * `enable_debug` When set, enables some - additional debugging features. Currently, this is only used to set the runtime profiling HTTP endpoints. + additional debugging features. Currently, this is only used to access runtime profiling HTTP endpoints, which + are available with an `operator:read` ACL regardles of the value of `enable_debug`. * `enable_script_checks` Equivalent to the [`-enable-script-checks` command-line flag](#_enable_script_checks). diff --git a/website/source/docs/commands/debug.html.markdown.erb b/website/source/docs/commands/debug.html.markdown.erb new file mode 100644 index 000000000..2f1457b19 --- /dev/null +++ b/website/source/docs/commands/debug.html.markdown.erb @@ -0,0 +1,122 @@ +--- +layout: "docs" +page_title: "Commands: Debug" +sidebar_current: "docs-commands-debug" +--- + +# Consul Debug + +Command: `consul debug` + +The `consul debug` command monitors a Consul agent for the specified period of +time, recording information about the agent, cluster, and environment to an archive +written to the current directory. + +Providing support for complex issues encountered by Consul operators often +requires a large amount of debugging information to be retrieved. This command +aims to shortcut that coordination and provide a simple workflow for accessing +data about Consul agent, cluster, and environment to enable faster +isolation and debugging of issues. + +This command requires an `operator:read` ACL token in order to retrieve the +data from the target agent, if ACLs are enabled. + +If the command is interrupted, as it could be given a long duration but +require less time than expected, it will attempt to archive the current +captured data. + +## Security and Privacy + +By default, ACL tokens, private keys, and other sensitive material related +to Consul is sanitized and not available in this archive. However, other +information about the environment the target agent is running in is available +in plain text within the archive. + +It is recommended to validate the contents of the archive and redact any +material classified as sensitive to the target environment, or use the `-capture` +flag to not retrieve it initially. + +Additionally, we recommend securely transmitting this archive via encryption +or otherwise. + +## Usage + +`Usage: consul debug [options]` + +By default, the debug command will capture an archive at the current path for +all targets for 2 minutes. + +#### API Options + +<%= partial "docs/commands/http_api_options_client" %> + +#### Command Options + +* `-duration` - Optional, the total time to capture data for from the target agent. Must + be greater than the interval and longer than 10 seconds. Defaults to 2 minutes. + +* `-interval` - Optional, the interval at which to capture dynamic data, such as logs + and metrics. Must be longer than 5 seconds. Defaults to 30 seconds. + +* `-capture` - Optional, can be specified multiple times for each [capture target](#capture-targets) + and will only record that information in the archive. + +* `-output` - Optional, the full path of where to write the directory of data and + resulting archive. Defaults to the current directory. + +* `-archive` - Optional, if the tool show archive the directory of data into a + compressed tar file. Defaults to true. + +## Capture Targets + +The `-capture` flag can be specified multiple times to capture specific +information when `debug` is running. By default, it captures all information. + +| Target | Description | +| ------ | ---------------------------- | +| `agent` | Version and configuration information about the agent. | +| `host` | Information about resources on the host running the target agent such as CPU, memory, and disk. | +| `cluster` | A list of all the WAN and LAN members in the cluster. | +| `metrics` | Metrics from the in-memory metrics endpoint in the target, captured at the interval. | +| `logs` | `DEBUG` level logs for the target agent, captured for the interval. | +| `pprof` | Golang heap, CPU, goroutine, and trace profiling. This information is not retrieved unless [`enable_debug`](/docs/agent/options.html#enable_debug) is set to `true` on the target agent. | + +## Examples + +This command can be run from any host with the Consul binary, but requires +network access to the target agent in order to retrieve data. Once retrieved, +the data is written to the the specified path (defaulting to the current +directory) on the host where the command runs. + +By default the command will capture all available data from the default +agent address on loopback for 2 minutes at 30 second intervals. + +```text +$ consul debug +... +``` + +In this example, the archive is collected from a different agent on the +network using the standard Consul CLI flag to change the API address. + +```text +$ consul debug -http-addr=10.0.1.10:8500 +... +``` + +The capture flag can be specified to only record a subset of data +about the agent and environment. + +```text +$ consul debug -capture agent -capture host -capture logs +... +``` + +The duration of the command and interval of capturing dynamic +information (such as metrics) can be specified with the `-interval` +and `-duration` flags. + +```text +$ consul debug -interval=15s -duration=1m +... +``` \ No newline at end of file diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 244d5c1ca..61f6b92c4 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -84,6 +84,9 @@ + > + debug + > event