-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add diagnostics collect command to elastic-agent. #28461
Changes from 2 commits
212ce03
4dcbc4f
043c8c8
4766260
500105a
f434c20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,18 +5,28 @@ | |
package cmd | ||
|
||
import ( | ||
"archive/zip" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"text/tabwriter" | ||
"time" | ||
|
||
"github.com/hashicorp/go-multierror" | ||
"github.com/spf13/cobra" | ||
"gopkg.in/yaml.v2" | ||
|
||
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/application/paths" | ||
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/configuration" | ||
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/control/client" | ||
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/agent/errors" | ||
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/cli" | ||
"github.com/elastic/beats/v7/x-pack/elastic-agent/pkg/config/operations" | ||
) | ||
|
||
var diagOutputs = map[string]outputter{ | ||
|
@@ -25,7 +35,19 @@ var diagOutputs = map[string]outputter{ | |
"yaml": yamlOutput, | ||
} | ||
|
||
func newDiagnosticsCommand(_ []string, streams *cli.IOStreams) *cobra.Command { | ||
// DiagnosticsInfo a struct to track all information related to diagnostics for the agent. | ||
type DiagnosticsInfo struct { | ||
ProcMeta []client.ProcMeta | ||
AgentVersion client.Version | ||
} | ||
|
||
// AgentConfig tracks all configuration that the agent uses, local files, rendered policies, beat inputs etc. | ||
type AgentConfig struct { | ||
ConfigLocal *configuration.Configuration | ||
ConfigRendered map[string]interface{} | ||
} | ||
|
||
func newDiagnosticsCommand(s []string, streams *cli.IOStreams) *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "diagnostics", | ||
Short: "Gather diagnostics information from the elastic-agent and running processes.", | ||
|
@@ -39,14 +61,41 @@ func newDiagnosticsCommand(_ []string, streams *cli.IOStreams) *cobra.Command { | |
} | ||
|
||
cmd.Flags().String("output", "human", "Output the diagnostics information in either human, json, or yaml (default: human)") | ||
cmd.AddCommand(newDiagnosticsCollectCommandWithArgs(s, streams)) | ||
|
||
return cmd | ||
} | ||
|
||
// DiagnosticsInfo a struct to track all inforation related to diagnostics for the agent. | ||
type DiagnosticsInfo struct { | ||
ProcMeta []client.ProcMeta | ||
AgentVersion client.Version | ||
func newDiagnosticsCollectCommandWithArgs(_ []string, streams *cli.IOStreams) *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "collect", | ||
Short: "Collect diagnostics information from the elastic-agent and write it to a zip archive.", | ||
Long: "Collect diagnostics information from the elastic-agent and write it to a zip archive.\nNote that any credentials will appear in plain text.", | ||
Args: cobra.MaximumNArgs(1), | ||
RunE: func(c *cobra.Command, args []string) error { | ||
file, _ := c.Flags().GetString("file") | ||
|
||
if file == "" { | ||
ts := time.Now().UTC() | ||
file = "elastic-agent-diagnostics-" + ts.Format(time.RFC3339) + ".zip" | ||
} | ||
|
||
output, _ := c.Flags().GetString("output") | ||
switch output { | ||
case "yaml": | ||
case "json": | ||
default: | ||
return fmt.Errorf("unsupported output: %s", output) | ||
} | ||
|
||
return diagnosticsCollectCmd(streams, file, output) | ||
}, | ||
} | ||
|
||
cmd.Flags().StringP("file", "f", "", "name of the output diagnostics zip archive") | ||
cmd.Flags().String("output", "yaml", "Output the collected information in either json, or yaml (default: yaml)") // replace output flag with different options | ||
|
||
return cmd | ||
} | ||
|
||
func diagnosticCmd(streams *cli.IOStreams, cmd *cobra.Command, args []string) error { | ||
|
@@ -77,6 +126,39 @@ func diagnosticCmd(streams *cli.IOStreams, cmd *cobra.Command, args []string) er | |
return outputFunc(streams.Out, diag) | ||
} | ||
|
||
func diagnosticsCollectCmd(streams *cli.IOStreams, fileName, outputFormat string) error { | ||
err := tryContainerLoadPaths() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
ctx := handleSignal(context.Background()) | ||
innerCtx, cancel := context.WithTimeout(ctx, 30*time.Second) | ||
defer cancel() | ||
|
||
diag, err := getDiagnostics(innerCtx) | ||
if err == context.DeadlineExceeded { | ||
return errors.New("timed out after 30 seconds trying to connect to Elastic Agent daemon") | ||
} else if err == context.Canceled { | ||
return nil | ||
} else if err != nil { | ||
return fmt.Errorf("failed to communicate with Elastic Agent daemon: %w", err) | ||
} | ||
|
||
cfg, err := gatherConfig() | ||
if err != nil { | ||
return fmt.Errorf("unable to gather config data: %w", err) | ||
} | ||
|
||
err = createZip(fileName, outputFormat, diag, cfg) | ||
if err != nil { | ||
return fmt.Errorf("unable to create archive %q: %w", fileName, err) | ||
} | ||
fmt.Fprintf(streams.Out, "Created diagnostics archive %q\n", fileName) | ||
fmt.Fprintln(streams.Out, "***** WARNING *****\nCreated archive may contain plain text credentials.\nEnsure that files in archive are redacted before sharing.\n*******************") | ||
return nil | ||
} | ||
|
||
func getDiagnostics(ctx context.Context) (DiagnosticsInfo, error) { | ||
daemon := client.New() | ||
diag := DiagnosticsInfo{} | ||
|
@@ -94,7 +176,7 @@ func getDiagnostics(ctx context.Context) (DiagnosticsInfo, error) { | |
|
||
version, err := daemon.Version(ctx) | ||
if err != nil { | ||
return DiagnosticsInfo{}, err | ||
return diag, err | ||
} | ||
diag.AgentVersion = version | ||
|
||
|
@@ -132,3 +214,155 @@ func outputDiagnostics(w io.Writer, d DiagnosticsInfo) error { | |
tw.Flush() | ||
return nil | ||
} | ||
|
||
func gatherConfig() (AgentConfig, error) { | ||
cfg := AgentConfig{} | ||
localCFG, err := loadConfig(nil) | ||
if err != nil { | ||
return cfg, err | ||
} | ||
cfg.ConfigLocal = localCFG | ||
|
||
renderedCFG, err := operations.LoadFullAgentConfig(paths.ConfigFile(), true) | ||
if err != nil { | ||
return cfg, err | ||
} | ||
// Must force *config.Config to map[string]interface{} in order to write to a file. | ||
mapCFG, err := renderedCFG.ToMapStr() | ||
if err != nil { | ||
return cfg, err | ||
} | ||
cfg.ConfigRendered = mapCFG | ||
|
||
return cfg, nil | ||
} | ||
|
||
// createZip creates a zip archive with the passed fileName. | ||
// | ||
// The passed DiagnosticsInfo and AgentConfig data is written in the specified output format. | ||
// Any local log files are collected and copied into the archive. | ||
func createZip(fileName, outputFormat string, diag DiagnosticsInfo, cfg AgentConfig) error { | ||
f, err := os.Create(fileName) | ||
if err != nil { | ||
return err | ||
} | ||
zw := zip.NewWriter(f) | ||
|
||
zf, err := zw.Create("meta/") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the docs for zip indicate that ending a name with a slash creates a directory. |
||
if err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
|
||
zf, err = zw.Create("meta/elastic-agent-version." + outputFormat) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tested on windows as well? i'm wondering because of |
||
if err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
if err := writeFile(zf, outputFormat, diag.AgentVersion); err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
|
||
for _, m := range diag.ProcMeta { | ||
zf, err = zw.Create("meta/" + m.Name + "-" + m.RouteKey + "." + outputFormat) | ||
if err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
|
||
if err := writeFile(zf, outputFormat, m); err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
} | ||
|
||
zf, err = zw.Create("config/") | ||
if err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
|
||
zf, err = zw.Create("config/elastic-agent-local." + outputFormat) | ||
if err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
if err := writeFile(zf, outputFormat, cfg.ConfigLocal); err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
|
||
zf, err = zw.Create("config/elastic-agent-policy." + outputFormat) | ||
if err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
if err := writeFile(zf, outputFormat, cfg.ConfigRendered); err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
|
||
if err := zipLogs(zw); err != nil { | ||
return closeHandlers(err, zw, f) | ||
} | ||
|
||
return closeHandlers(nil, zw, f) | ||
} | ||
|
||
// zipLogs walks paths.Logs() and copies the file structure into zw in "logs/" | ||
func zipLogs(zw *zip.Writer) error { | ||
_, err := zw.Create("logs/") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// using Data() + "/logs", for some reason default paths/Logs() is the home dir... | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused as to why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logPath defaults to topPath which is what you see if no override is provided |
||
logPath := filepath.Join(paths.Data(), "logs") + string(filepath.Separator) | ||
return filepath.WalkDir(logPath, func(path string, d fs.DirEntry, fErr error) error { | ||
if fErr != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's hande IsNotExist(err) and continue without processing if so. i've had issues with Walk before and this is not serious but can break the process |
||
return fmt.Errorf("unable to walk log dir: %w", fErr) | ||
} | ||
name := strings.TrimPrefix(path, logPath) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if name == "" { | ||
return nil | ||
} | ||
|
||
if d.IsDir() { | ||
_, err := zw.Create("logs/" + name + "/") | ||
if err != nil { | ||
return fmt.Errorf("unable to create log directory in archive: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
lf, err := os.Open(path) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not closed, better to extract to a func and use defer to close the file There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done, just added closeHandlers to my exit points |
||
if err != nil { | ||
return fmt.Errorf("unable to open log file: %w", err) | ||
} | ||
zf, err := zw.Create("logs/" + name) | ||
if err != nil { | ||
return fmt.Errorf("unable to create log file in archive: %w", err) | ||
} | ||
_, err = io.Copy(zf, lf) | ||
if err != nil { | ||
return fmt.Errorf("log file copy failed: %w", err) | ||
} | ||
|
||
return nil | ||
}) | ||
} | ||
|
||
// writeFile writes json or yaml data from the interface to the writer. | ||
func writeFile(w io.Writer, outputFormat string, v interface{}) error { | ||
if outputFormat == "yaml" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not serious at all but we default to yaml when using a command, we could also use json here if specified and default to yaml as well |
||
ye := yaml.NewEncoder(w) | ||
err := ye.Encode(v) | ||
return closeHandlers(err, ye) | ||
} | ||
je := json.NewEncoder(w) | ||
je.SetIndent("", " ") | ||
return je.Encode(v) | ||
} | ||
|
||
// closeHandlers will close all passed closers attaching any errors to the passed err and returning the result | ||
func closeHandlers(err error, closers ...io.Closer) error { | ||
var mErr *multierror.Error | ||
mErr = multierror.Append(mErr, err) | ||
for _, c := range closers { | ||
if inErr := c.Close(); inErr != nil { | ||
mErr = multierror.Append(mErr, inErr) | ||
} | ||
} | ||
return mErr.ErrorOrNil() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/archieve/archive