Merge pull request #10804 from hashicorp/dnephin/debug-filenames

debug: rename cluster.json -> members.json  and fix handling of Interrupt Signal
pull/10874/head
Daniel Nephin 2021-08-18 13:18:29 -04:00 committed by GitHub
commit a98b5bc31c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 206 additions and 171 deletions

3
.changelog/10804.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
debug: rename cluster capture target to members, to be more consistent with the terms used by the API.
```

View File

@ -1,7 +1,9 @@
package api package api
import ( import (
"context"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
) )
@ -70,6 +72,26 @@ func (d *Debug) Profile(seconds int) ([]byte, error) {
return body, nil return body, nil
} }
// PProf returns a pprof profile for the specified number of seconds. The caller
// is responsible for closing the returned io.ReadCloser once all bytes are read.
func (d *Debug) PProf(ctx context.Context, name string, seconds int) (io.ReadCloser, error) {
r := d.c.newRequest("GET", "/debug/pprof/"+name)
r.ctx = ctx
// 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)
}
if resp.StatusCode != 200 {
return nil, generateUnexpectedResponseCodeError(resp)
}
return resp.Body, nil
}
// Trace returns an execution trace // Trace returns an execution trace
func (d *Debug) Trace(seconds int) ([]byte, error) { func (d *Debug) Trace(seconds int) ([]byte, error) {
r := d.c.newRequest("GET", "/debug/pprof/trace") r := d.c.newRequest("GET", "/debug/pprof/trace")

View File

@ -166,7 +166,7 @@ func init() {
Register("connect envoy pipe-bootstrap", func(ui cli.Ui) (cli.Command, error) { return pipebootstrap.New(ui), nil }) Register("connect envoy pipe-bootstrap", func(ui cli.Ui) (cli.Command, error) { return pipebootstrap.New(ui), nil })
Register("connect expose", func(ui cli.Ui) (cli.Command, error) { return expose.New(ui), nil }) Register("connect expose", func(ui cli.Ui) (cli.Command, error) { return expose.New(ui), nil })
Register("connect redirect-traffic", func(ui cli.Ui) (cli.Command, error) { return redirecttraffic.New(ui), nil }) Register("connect redirect-traffic", func(ui cli.Ui) (cli.Command, error) { return redirecttraffic.New(ui), nil })
Register("debug", func(ui cli.Ui) (cli.Command, error) { return debug.New(ui, MakeShutdownCh()), nil }) Register("debug", func(ui cli.Ui) (cli.Command, error) { return debug.New(ui), nil })
Register("event", func(ui cli.Ui) (cli.Command, error) { return event.New(ui), 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("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 }) Register("force-leave", func(ui cli.Ui) (cli.Command, error) { return forceleave.New(ui), nil })

View File

@ -12,8 +12,10 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall"
"time" "time"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -55,7 +57,7 @@ const (
debugProtocolVersion = 1 debugProtocolVersion = 1
) )
func New(ui cli.Ui, shutdownCh <-chan struct{}) *cmd { func New(ui cli.Ui) *cmd {
ui = &cli.PrefixedUi{ ui = &cli.PrefixedUi{
OutputPrefix: "==> ", OutputPrefix: "==> ",
InfoPrefix: " ", InfoPrefix: " ",
@ -63,7 +65,7 @@ func New(ui cli.Ui, shutdownCh <-chan struct{}) *cmd {
Ui: ui, Ui: ui,
} }
c := &cmd{UI: ui, shutdownCh: shutdownCh} c := &cmd{UI: ui}
c.init() c.init()
return c return c
} }
@ -74,8 +76,6 @@ type cmd struct {
http *flags.HTTPFlags http *flags.HTTPFlags
help string help string
shutdownCh <-chan struct{}
// flags // flags
interval time.Duration interval time.Duration
duration time.Duration duration time.Duration
@ -114,7 +114,7 @@ func (c *cmd) init() {
fmt.Sprintf("One or more types of information to capture. This can be used "+ fmt.Sprintf("One or more types of information to capture. This can be used "+
"to capture a subset of information, and defaults to capturing "+ "to capture a subset of information, and defaults to capturing "+
"everything available. Possible information for capture: %s. "+ "everything available. Possible information for capture: %s. "+
"This can be repeated multiple times.", strings.Join(c.defaultTargets(), ", "))) "This can be repeated multiple times.", strings.Join(defaultTargets, ", ")))
c.flags.DurationVar(&c.interval, "interval", debugInterval, c.flags.DurationVar(&c.interval, "interval", debugInterval,
fmt.Sprintf("The interval in which to capture dynamic information such as "+ fmt.Sprintf("The interval in which to capture dynamic information such as "+
"telemetry, and profiling. Defaults to %s.", debugInterval)) "telemetry, and profiling. Defaults to %s.", debugInterval))
@ -136,6 +136,9 @@ func (c *cmd) init() {
} }
func (c *cmd) Run(args []string) int { func (c *cmd) Run(args []string) int {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
if err := c.flags.Parse(args); err != nil { if err := c.flags.Parse(args); err != nil {
c.UI.Error(fmt.Sprintf("Error parsing flags: %s", err)) c.UI.Error(fmt.Sprintf("Error parsing flags: %s", err))
return 1 return 1
@ -195,10 +198,14 @@ func (c *cmd) Run(args []string) int {
} }
// Capture dynamic information from the target agent, blocking for duration // Capture dynamic information from the target agent, blocking for duration
if c.configuredTarget("metrics") || c.configuredTarget("logs") || c.configuredTarget("pprof") { if c.captureTarget(targetMetrics) || c.captureTarget(targetLogs) || c.captureTarget(targetProfiles) {
g := new(errgroup.Group) g := new(errgroup.Group)
g.Go(c.captureInterval) g.Go(func() error {
g.Go(c.captureLongRunning) return c.captureInterval(ctx)
})
g.Go(func() error {
return c.captureLongRunning(ctx)
})
err = g.Wait() err = g.Wait()
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error encountered during collection: %v", err)) c.UI.Error(fmt.Sprintf("Error encountered during collection: %v", err))
@ -264,11 +271,11 @@ func (c *cmd) prepare() (version string, err error) {
// If none are specified we will collect information from // If none are specified we will collect information from
// all by default // all by default
if len(c.capture) == 0 { if len(c.capture) == 0 {
c.capture = c.defaultTargets() c.capture = defaultTargets
} }
for _, t := range c.capture { for _, t := range c.capture {
if !c.allowedTarget(t) { if !allowedTarget(t) {
return version, fmt.Errorf("target not found: %s", t) return version, fmt.Errorf("target not found: %s", t)
} }
} }
@ -288,60 +295,52 @@ func (c *cmd) prepare() (version string, err error) {
// captureStatic captures static target information and writes it // captureStatic captures static target information and writes it
// to the output path // to the output path
func (c *cmd) captureStatic() error { func (c *cmd) captureStatic() error {
// Collect errors via multierror as we want to gracefully
// fail if an API is inaccessible
var errs error var errs error
// Collect the named outputs here if c.captureTarget(targetHost) {
outputs := make(map[string]interface{})
// Capture host information
if c.configuredTarget("host") {
host, err := c.client.Agent().Host() host, err := c.client.Agent().Host()
if err != nil { if err != nil {
errs = multierror.Append(errs, err) errs = multierror.Append(errs, err)
} }
outputs["host"] = host if err := writeJSONFile(filepath.Join(c.output, targetHost+".json"), host); err != nil {
errs = multierror.Append(errs, err)
}
} }
// Capture agent information if c.captureTarget(targetAgent) {
if c.configuredTarget("agent") {
agent, err := c.client.Agent().Self() agent, err := c.client.Agent().Self()
if err != nil { if err != nil {
errs = multierror.Append(errs, err) errs = multierror.Append(errs, err)
} }
outputs["agent"] = agent if err := writeJSONFile(filepath.Join(c.output, targetAgent+".json"), agent); err != nil {
errs = multierror.Append(errs, err)
}
} }
// Capture cluster members information, including WAN if c.captureTarget(targetMembers) {
if c.configuredTarget("cluster") {
members, err := c.client.Agent().Members(true) members, err := c.client.Agent().Members(true)
if err != nil { if err != nil {
errs = multierror.Append(errs, err) errs = multierror.Append(errs, err)
} }
outputs["cluster"] = members if err := writeJSONFile(filepath.Join(c.output, targetMembers+".json"), members); err != nil {
}
// Write all outputs to disk as JSON
for output, v := range outputs {
marshaled, err := json.MarshalIndent(v, "", "\t")
if err != nil {
errs = multierror.Append(errs, err)
}
err = ioutil.WriteFile(fmt.Sprintf("%s/%s.json", c.output, output), marshaled, 0644)
if err != nil {
errs = multierror.Append(errs, err) errs = multierror.Append(errs, err)
} }
} }
return errs return errs
} }
func writeJSONFile(filename string, content interface{}) error {
marshaled, err := json.MarshalIndent(content, "", "\t")
if err != nil {
return err
}
return ioutil.WriteFile(filename, marshaled, 0644)
}
// captureInterval blocks for the duration of the command // captureInterval blocks for the duration of the command
// specified by the duration flag, capturing the dynamic // specified by the duration flag, capturing the dynamic
// targets at the interval specified // targets at the interval specified
func (c *cmd) captureInterval() error { func (c *cmd) captureInterval(ctx context.Context) error {
intervalChn := time.NewTicker(c.interval) intervalChn := time.NewTicker(c.interval)
defer intervalChn.Stop() defer intervalChn.Stop()
durationChn := time.After(c.duration) durationChn := time.After(c.duration)
@ -366,7 +365,7 @@ func (c *cmd) captureInterval() error {
case <-durationChn: case <-durationChn:
intervalChn.Stop() intervalChn.Stop()
return nil return nil
case <-c.shutdownCh: case <-ctx.Done():
return errors.New("stopping collection due to shutdown signal") return errors.New("stopping collection due to shutdown signal")
} }
} }
@ -380,7 +379,7 @@ func captureShortLived(c *cmd) error {
if err != nil { if err != nil {
return err return err
} }
if c.configuredTarget("pprof") { if c.captureTarget(targetProfiles) {
g.Go(func() error { g.Go(func() error {
return c.captureHeap(timestampDir) return c.captureHeap(timestampDir)
}) })
@ -403,7 +402,7 @@ func (c *cmd) createTimestampDir(timestamp int64) (string, error) {
return timestampDir, nil return timestampDir, nil
} }
func (c *cmd) captureLongRunning() error { func (c *cmd) captureLongRunning(ctx context.Context) error {
timestamp := time.Now().Local().Unix() timestamp := time.Now().Local().Unix()
timestampDir, err := c.createTimestampDir(timestamp) timestampDir, err := c.createTimestampDir(timestamp)
@ -417,26 +416,29 @@ func (c *cmd) captureLongRunning() error {
if s < 1 { if s < 1 {
s = 1 s = 1
} }
if c.configuredTarget("pprof") { if c.captureTarget(targetProfiles) {
g.Go(func() error { g.Go(func() error {
return c.captureProfile(s, timestampDir) // use ctx without a timeout to allow the profile to finish sending
return c.captureProfile(ctx, s, timestampDir)
}) })
g.Go(func() error { g.Go(func() error {
return c.captureTrace(s, timestampDir) // use ctx without a timeout to allow the trace to finish sending
return c.captureTrace(ctx, s, timestampDir)
}) })
} }
if c.configuredTarget("logs") { if c.captureTarget(targetLogs) {
g.Go(func() error { g.Go(func() error {
return c.captureLogs(timestampDir) ctx, cancel := context.WithTimeout(ctx, c.duration)
})
}
if c.configuredTarget("metrics") {
// TODO: pass in context from caller
ctx, cancel := context.WithTimeout(context.Background(), c.duration)
defer cancel() defer cancel()
return c.captureLogs(ctx, timestampDir)
})
}
if c.captureTarget(targetMetrics) {
g.Go(func() error { g.Go(func() error {
ctx, cancel := context.WithTimeout(ctx, c.duration)
defer cancel()
return c.captureMetrics(ctx, timestampDir) return c.captureMetrics(ctx, timestampDir)
}) })
} }
@ -450,27 +452,40 @@ func (c *cmd) captureGoRoutines(timestampDir string) error {
return fmt.Errorf("failed to collect goroutine profile: %w", err) return fmt.Errorf("failed to collect goroutine profile: %w", err)
} }
err = ioutil.WriteFile(fmt.Sprintf("%s/goroutine.prof", timestampDir), gr, 0644) return ioutil.WriteFile(fmt.Sprintf("%s/goroutine.prof", timestampDir), gr, 0644)
return err
} }
func (c *cmd) captureTrace(s float64, timestampDir string) error { func (c *cmd) captureTrace(ctx context.Context, s float64, timestampDir string) error {
trace, err := c.client.Debug().Trace(int(s)) prof, err := c.client.Debug().PProf(ctx, "trace", int(s))
if err != nil {
return fmt.Errorf("failed to collect trace: %w", err)
}
err = ioutil.WriteFile(fmt.Sprintf("%s/trace.out", timestampDir), trace, 0644)
return err
}
func (c *cmd) captureProfile(s float64, timestampDir string) error {
prof, err := c.client.Debug().Profile(int(s))
if err != nil { if err != nil {
return fmt.Errorf("failed to collect cpu profile: %w", err) return fmt.Errorf("failed to collect cpu profile: %w", err)
} }
defer prof.Close()
err = ioutil.WriteFile(fmt.Sprintf("%s/profile.prof", timestampDir), prof, 0644) r := bufio.NewReader(prof)
fh, err := os.Create(fmt.Sprintf("%s/trace.out", timestampDir))
if err != nil {
return err
}
defer fh.Close()
_, err = r.WriteTo(fh)
return err
}
func (c *cmd) captureProfile(ctx context.Context, s float64, timestampDir string) error {
prof, err := c.client.Debug().PProf(ctx, "profile", int(s))
if err != nil {
return fmt.Errorf("failed to collect cpu profile: %w", err)
}
defer prof.Close()
r := bufio.NewReader(prof)
fh, err := os.Create(fmt.Sprintf("%s/profile.prof", timestampDir))
if err != nil {
return err
}
defer fh.Close()
_, err = r.WriteTo(fh)
return err return err
} }
@ -480,19 +495,14 @@ func (c *cmd) captureHeap(timestampDir string) error {
return fmt.Errorf("failed to collect heap profile: %w", err) return fmt.Errorf("failed to collect heap profile: %w", err)
} }
err = ioutil.WriteFile(fmt.Sprintf("%s/heap.prof", timestampDir), heap, 0644) return ioutil.WriteFile(fmt.Sprintf("%s/heap.prof", timestampDir), heap, 0644)
return err
} }
func (c *cmd) captureLogs(timestampDir string) error { func (c *cmd) captureLogs(ctx context.Context, timestampDir string) error {
endLogChn := make(chan struct{}) logCh, err := c.client.Agent().Monitor("DEBUG", ctx.Done(), nil)
timeIsUp := time.After(c.duration)
logCh, err := c.client.Agent().Monitor("DEBUG", endLogChn, nil)
if err != nil { if err != nil {
return err return err
} }
// Close the log stream
defer close(endLogChn)
// Create the log file for writing // Create the log file for writing
f, err := os.Create(fmt.Sprintf("%s/%s", timestampDir, "consul.log")) f, err := os.Create(fmt.Sprintf("%s/%s", timestampDir, "consul.log"))
@ -510,7 +520,7 @@ func (c *cmd) captureLogs(timestampDir string) error {
if _, err = f.WriteString(log + "\n"); err != nil { if _, err = f.WriteString(log + "\n"); err != nil {
return err return err
} }
case <-timeIsUp: case <-ctx.Done():
return nil return nil
} }
} }
@ -538,23 +548,31 @@ func (c *cmd) captureMetrics(ctx context.Context, timestampDir string) error {
return nil return nil
} }
// allowedTarget returns a boolean if the target is able to be captured // allowedTarget returns true if the target is a recognized name of a capture
func (c *cmd) allowedTarget(target string) bool { // target.
for _, dt := range c.defaultTargets() { func allowedTarget(target string) bool {
for _, dt := range defaultTargets {
if dt == target { if dt == target {
return true return true
} }
} }
for _, t := range deprecatedTargets {
if t == target {
return true
}
}
return false return false
} }
// configuredTarget returns a boolean if the target is configured to be // captureTarget returns true if the target capture type is enabled.
// captured in the command func (c *cmd) captureTarget(target string) bool {
func (c *cmd) configuredTarget(target string) bool {
for _, dt := range c.capture { for _, dt := range c.capture {
if dt == target { if dt == target {
return true return true
} }
if target == targetMembers && dt == targetCluster {
return true
}
} }
return false return false
} }
@ -676,33 +694,37 @@ func (c *cmd) createArchiveTemp(path string) (tempName string, err error) {
return tempName, nil return tempName, nil
} }
// defaultTargets specifies the list of all targets that const (
// will be captured by default targetMetrics = "metrics"
func (c *cmd) defaultTargets() []string { targetLogs = "logs"
return append(c.dynamicTargets(), c.staticTargets()...) targetProfiles = "pprof"
targetHost = "host"
targetAgent = "agent"
targetMembers = "members"
// targetCluster is the now deprecated name for targetMembers
targetCluster = "cluster"
)
// defaultTargets specifies the list of targets that will be captured by default
var defaultTargets = []string{
targetMetrics,
targetLogs,
targetProfiles,
targetHost,
targetAgent,
targetMembers,
} }
// dynamicTargets returns all the supported targets var deprecatedTargets = []string{targetCluster}
// 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 { func (c *cmd) Synopsis() string {
return synopsis return "Records a debugging archive for operators"
} }
func (c *cmd) Help() string { func (c *cmd) Help() string {
return c.help return c.help
} }
const synopsis = "Records a debugging archive for operators"
const help = ` const help = `
Usage: consul debug [options] Usage: consul debug [options]

View File

@ -14,20 +14,19 @@ import (
"testing" "testing"
"time" "time"
"github.com/google/pprof/profile"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
"github.com/google/pprof/profile" "gotest.tools/v3/fs"
"github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/testrpc"
) )
func TestDebugCommand_noTabs(t *testing.T) { func TestDebugCommand_Help_TextContainsNoTabs(t *testing.T) {
t.Parallel() if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
if strings.ContainsRune(New(cli.NewMockUi(), nil).Help(), '\t') {
t.Fatal("help has tabs") t.Fatal("help has tabs")
} }
} }
@ -47,7 +46,7 @@ func TestDebugCommand(t *testing.T) {
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := fmt.Sprintf("%s/debug", testDir) outputPath := fmt.Sprintf("%s/debug", testDir)
@ -63,6 +62,16 @@ func TestDebugCommand(t *testing.T) {
require.Equal(t, 0, code) require.Equal(t, 0, code)
require.Equal(t, "", ui.ErrorWriter.String()) require.Equal(t, "", ui.ErrorWriter.String())
expected := fs.Expected(t,
fs.WithDir("debug",
fs.WithFile("agent.json", "", fs.MatchAnyFileContent),
fs.WithFile("host.json", "", fs.MatchAnyFileContent),
fs.WithFile("index.json", "", fs.MatchAnyFileContent),
fs.WithFile("members.json", "", fs.MatchAnyFileContent),
// TODO: make the sub-directory names predictable)
fs.MatchExtraFiles))
assert.Assert(t, fs.Equal(testDir, expected))
metricsFiles, err := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, "metrics.json")) metricsFiles, err := filepath.Glob(fmt.Sprintf("%s/*/%s", outputPath, "metrics.json"))
require.NoError(t, err) require.NoError(t, err)
require.Len(t, metricsFiles, 1) require.Len(t, metricsFiles, 1)
@ -83,7 +92,7 @@ func TestDebugCommand_Archive(t *testing.T) {
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := fmt.Sprintf("%s/debug", testDir) outputPath := fmt.Sprintf("%s/debug", testDir)
@ -127,15 +136,10 @@ func TestDebugCommand_Archive(t *testing.T) {
} }
func TestDebugCommand_ArgsBad(t *testing.T) { func TestDebugCommand_ArgsBad(t *testing.T) {
t.Parallel()
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
args := []string{ args := []string{"foo", "bad"}
"foo",
"bad",
}
if code := cmd.Run(args); code == 0 { if code := cmd.Run(args); code == 0 {
t.Fatalf("should exit non-zero, got code: %d", code) t.Fatalf("should exit non-zero, got code: %d", code)
@ -149,7 +153,7 @@ func TestDebugCommand_ArgsBad(t *testing.T) {
func TestDebugCommand_InvalidFlags(t *testing.T) { func TestDebugCommand_InvalidFlags(t *testing.T) {
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := "" outputPath := ""
@ -182,7 +186,7 @@ func TestDebugCommand_OutputPathBad(t *testing.T) {
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := "" outputPath := ""
@ -215,7 +219,7 @@ func TestDebugCommand_OutputPathExists(t *testing.T) {
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := fmt.Sprintf("%s/debug", testDir) outputPath := fmt.Sprintf("%s/debug", testDir)
@ -258,17 +262,17 @@ func TestDebugCommand_CaptureTargets(t *testing.T) {
"single": { "single": {
[]string{"agent"}, []string{"agent"},
[]string{"agent.json"}, []string{"agent.json"},
[]string{"host.json", "cluster.json"}, []string{"host.json", "members.json"},
}, },
"static": { "static": {
[]string{"agent", "host", "cluster"}, []string{"agent", "host", "cluster"},
[]string{"agent.json", "host.json", "cluster.json"}, []string{"agent.json", "host.json", "members.json"},
[]string{"*/metrics.json"}, []string{"*/metrics.json"},
}, },
"metrics-only": { "metrics-only": {
[]string{"metrics"}, []string{"metrics"},
[]string{"*/metrics.json"}, []string{"*/metrics.json"},
[]string{"agent.json", "host.json", "cluster.json"}, []string{"agent.json", "host.json", "members.json"},
}, },
"all-but-pprof": { "all-but-pprof": {
[]string{ []string{
@ -281,7 +285,7 @@ func TestDebugCommand_CaptureTargets(t *testing.T) {
[]string{ []string{
"host.json", "host.json",
"agent.json", "agent.json",
"cluster.json", "members.json",
"*/metrics.json", "*/metrics.json",
"*/consul.log", "*/consul.log",
}, },
@ -300,7 +304,7 @@ func TestDebugCommand_CaptureTargets(t *testing.T) {
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := fmt.Sprintf("%s/debug-%s", testDir, name) outputPath := fmt.Sprintf("%s/debug-%s", testDir, name)
@ -383,7 +387,7 @@ func TestDebugCommand_CaptureLogs(t *testing.T) {
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := fmt.Sprintf("%s/debug-%s", testDir, name) outputPath := fmt.Sprintf("%s/debug-%s", testDir, name)
@ -476,7 +480,7 @@ func TestDebugCommand_ProfilesExist(t *testing.T) {
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := fmt.Sprintf("%s/debug", testDir) outputPath := fmt.Sprintf("%s/debug", testDir)
@ -518,65 +522,44 @@ func TestDebugCommand_ProfilesExist(t *testing.T) {
} }
} }
func TestDebugCommand_ValidateTiming(t *testing.T) { func TestDebugCommand_Prepare_ValidateTiming(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
cases := map[string]struct { cases := map[string]struct {
duration string duration string
interval string interval string
output string expected string
code int
}{ }{
"both": { "both": {
"20ms", duration: "20ms",
"10ms", interval: "10ms",
"duration must be longer", expected: "duration must be longer",
1,
}, },
"short interval": { "short interval": {
"10s", duration: "10s",
"10ms", interval: "10ms",
"interval must be longer", expected: "interval must be longer",
1,
}, },
"lower duration": { "lower duration": {
"20s", duration: "20s",
"30s", interval: "30s",
"must be longer than interval", expected: "must be longer than interval",
1,
}, },
} }
for name, tc := range cases { for name, tc := range cases {
// Because we're only testng validation, we want to shut down t.Run(name, func(t *testing.T) {
// the valid duration test to avoid hanging
shutdownCh := make(chan struct{})
a := agent.NewTestAgent(t, "")
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, shutdownCh) cmd := New(ui)
args := []string{ args := []string{
"-http-addr=" + a.HTTPAddr(),
"-duration=" + tc.duration, "-duration=" + tc.duration,
"-interval=" + tc.interval, "-interval=" + tc.interval,
"-capture=agent",
} }
code := cmd.Run(args) err := cmd.flags.Parse(args)
require.NoError(t, err)
if code != tc.code { _, err = cmd.prepare()
t.Errorf("%s: should exit %d, got code: %d", name, tc.code, code) testutil.RequireErrorContains(t, err, tc.expected)
} })
errOutput := ui.ErrorWriter.String()
if !strings.Contains(errOutput, tc.output) {
t.Errorf("%s: expected error output '%s', got '%q'", name, tc.output, errOutput)
}
} }
} }
@ -596,7 +579,7 @@ func TestDebugCommand_DebugDisabled(t *testing.T) {
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi() ui := cli.NewMockUi()
cmd := New(ui, nil) cmd := New(ui)
cmd.validateTiming = false cmd.validateTiming = false
outputPath := fmt.Sprintf("%s/debug", testDir) outputPath := fmt.Sprintf("%s/debug", testDir)

View File

@ -47,6 +47,7 @@ var registry map[string]Factory
// MakeShutdownCh returns a channel that can be used for shutdown notifications // MakeShutdownCh returns a channel that can be used for shutdown notifications
// for commands. This channel will send a message for every interrupt or SIGTERM // for commands. This channel will send a message for every interrupt or SIGTERM
// received. // received.
// Deprecated: use signal.NotifyContext
func MakeShutdownCh() <-chan struct{} { func MakeShutdownCh() <-chan struct{} {
resultCh := make(chan struct{}) resultCh := make(chan struct{})
signalCh := make(chan os.Signal, 4) signalCh := make(chan os.Signal, 4)

1
go.mod
View File

@ -92,6 +92,7 @@ require (
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55
google.golang.org/grpc v1.25.1 google.golang.org/grpc v1.25.1
gopkg.in/square/go-jose.v2 v2.5.1 gopkg.in/square/go-jose.v2 v2.5.1
gotest.tools/v3 v3.0.3
k8s.io/api v0.16.9 k8s.io/api v0.16.9
k8s.io/apimachinery v0.16.9 k8s.io/apimachinery v0.16.9
k8s.io/client-go v0.16.9 k8s.io/client-go v0.16.9

3
go.sum
View File

@ -652,6 +652,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -710,6 +711,8 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -8,13 +8,13 @@ page_title: 'Commands: Debug'
Command: `consul debug` Command: `consul debug`
The `consul debug` command monitors a Consul agent for the specified period of 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 time, recording information about the agent, cluster membership, and environment to an archive
written to the current directory. written to the current directory.
Providing support for complex issues encountered by Consul operators often Providing support for complex issues encountered by Consul operators often
requires a large amount of debugging information to be retrieved. This command requires a large amount of debugging information to be retrieved. This command
aims to shortcut that coordination and provide a simple workflow for accessing aims to shortcut that coordination and provide a simple workflow for accessing
data about Consul agent, cluster, and environment to enable faster data about Consul agent, cluster membership, and environment to enable faster
isolation and debugging of issues. isolation and debugging of issues.
This command requires an `operator:read` ACL token in order to retrieve the This command requires an `operator:read` ACL token in order to retrieve the
@ -75,7 +75,7 @@ information when `debug` is running. By default, it captures all information.
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent` | Version and configuration information about the agent. | | `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. | | `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. | | `members` | 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. | | `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 duration. | | `logs` | `DEBUG` level logs for the target agent, captured for the duration. |
| `pprof` | Golang heap, CPU, goroutine, and trace profiling. CPU and traces are captured for `duration` in a single file while heap and goroutine are separate snapshots for each `interval`. This information is not retrieved unless [`enable_debug`](/docs/agent/options#enable_debug) is set to `true` on the target agent or ACLs are enable and an ACL token with `operator:read` is provided. | | `pprof` | Golang heap, CPU, goroutine, and trace profiling. CPU and traces are captured for `duration` in a single file while heap and goroutine are separate snapshots for each `interval`. This information is not retrieved unless [`enable_debug`](/docs/agent/options#enable_debug) is set to `true` on the target agent or ACLs are enable and an ACL token with `operator:read` is provided. |