internal/pkg/agent/cmd/diagnostics.go (105 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package cmd
import (
"context"
"fmt"
"os"
"path"
"time"
"github.com/elastic/elastic-agent/pkg/control/v2/client"
"github.com/elastic/elastic-agent/pkg/control/v2/cproto"
"github.com/spf13/cobra"
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
"github.com/elastic/elastic-agent/internal/pkg/cli"
"github.com/elastic/elastic-agent/internal/pkg/diagnostics"
)
func newDiagnosticsCommand(_ []string, streams *cli.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "diagnostics",
Short: "Gather diagnostics information from the Elastic Agent and write it to a zip archive",
Long: "This command gathers diagnostics information from the Elastic Agent and writes it to a zip archive.",
Run: func(c *cobra.Command, args []string) {
if err := diagnosticCmd(streams, c); err != nil {
fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage())
os.Exit(1)
}
},
}
cmd.Flags().StringP("file", "f", "", "name of the output diagnostics zip archive")
cmd.Flags().BoolP("cpu-profile", "p", false, "wait to collect a CPU profile")
cmd.Flags().BoolP("skip-conn", "", false, "Skip connection request diagnostics")
cmd.Flags().Bool("exclude-events", false, "do not collect events log file")
return cmd
}
func diagnosticCmd(streams *cli.IOStreams, cmd *cobra.Command) error {
filepath, _ := cmd.Flags().GetString("file")
if filepath == "" {
ts := time.Now().UTC()
filepath = "elastic-agent-diagnostics-" + ts.Format("2006-01-02T15-04-05Z07-00") + ".zip" // RFC3339 format that replaces : with -, so it will work on Windows
}
excludeEvents, err := cmd.Flags().GetBool("exclude-events")
if err != nil {
return fmt.Errorf("cannot get 'exclude-events' flag: %w", err)
}
ctx := handleSignal(context.Background())
// 1st create the file to store the diagnostics, if it fails, anything else
// is pointless.
f, err := createFile(filepath)
if err != nil {
return fmt.Errorf("could not create diagnostics file %q: %w", filepath, err)
}
defer f.Close()
cpuProfile, _ := cmd.Flags().GetBool("cpu-profile")
connSkip, _ := cmd.Flags().GetBool("skip-conn")
agentDiag, unitDiags, compDiags, err := collectDiagnostics(ctx, streams, cpuProfile, connSkip)
if err != nil {
return fmt.Errorf("failed collecting diagnostics: %w", err)
}
if err := diagnostics.ZipArchive(streams.Err, f, paths.Top(), agentDiag, unitDiags, compDiags, excludeEvents); err != nil {
return fmt.Errorf("unable to create archive %q: %w", filepath, err)
}
fmt.Fprintf(streams.Out, "Created diagnostics archive %q\n", filepath)
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 collectDiagnostics(ctx context.Context, streams *cli.IOStreams, cpuProfile, connSkip bool) ([]client.DiagnosticFileResult, []client.DiagnosticUnitResult, []client.DiagnosticComponentResult, error) {
daemon := client.New()
err := daemon.Connect(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to connect to daemon: %w", err)
}
defer daemon.Disconnect()
var additionalDiags []cproto.AdditionalDiagnosticRequest
if !connSkip {
additionalDiags = append(additionalDiags, cproto.AdditionalDiagnosticRequest_CONN)
}
if cpuProfile {
// console will just hang while we wait for the CPU profile; print something so user doesn't get confused
fmt.Fprintf(streams.Out, "Creating diagnostics archive, waiting for CPU profile...\n")
additionalDiags = append(additionalDiags, cproto.AdditionalDiagnosticRequest_CPU)
}
agentDiag, err := daemon.DiagnosticAgent(ctx, additionalDiags)
if err != nil {
fmt.Fprintf(streams.Err, "[WARNING]: failed to fetch agent diagnostics: %s", err)
}
unitDiags, err := daemon.DiagnosticUnits(ctx)
if err != nil {
fmt.Fprintf(streams.Err, "[WARNING]: failed to fetch unit diagnostics: %s", err)
}
compDiags, err := daemon.DiagnosticComponents(ctx, additionalDiags)
if err != nil {
fmt.Fprintf(streams.Err, "[WARNING]: failed to fetch component diagnostics: %s", err)
}
if len(compDiags) == 0 && len(unitDiags) == 0 && len(agentDiag) == 0 {
return nil, nil, nil, fmt.Errorf("no diags could be fetched")
}
return agentDiag, unitDiags, compDiags, nil
}
func createFile(filepath string) (*os.File, error) {
// Ensure all the folders on filepath exist as os.Create does not do so.
// 0777 is the same permission, before unmask, os.Create uses.
dir := path.Dir(filepath)
if err := os.MkdirAll(dir, 0777); err != nil {
return nil, fmt.Errorf("could not create folders to save diagnostics on %q: %w",
dir, err)
}
f, err := os.Create(filepath)
if err != nil {
return nil, fmt.Errorf("error creating .zip file: %w", err)
}
return f, nil
}