|
| 1 | +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one |
| 2 | +// or more contributor license agreements. Licensed under the Elastic License 2.0; |
| 3 | +// you may not use this file except in compliance with the Elastic License 2.0. |
| 4 | + |
| 5 | +package cmd |
| 6 | + |
| 7 | +import ( |
| 8 | + "fmt" |
| 9 | + "os" |
| 10 | + "time" |
| 11 | + |
| 12 | + "github.com/spf13/cobra" |
| 13 | + |
| 14 | + "github.com/elastic/elastic-agent/internal/pkg/agent/application/paths" |
| 15 | + "github.com/elastic/elastic-agent/internal/pkg/cli" |
| 16 | + "github.com/elastic/elastic-agent/internal/pkg/diagnostics" |
| 17 | + "github.com/elastic/elastic-agent/internal/pkg/otel" |
| 18 | + "github.com/elastic/elastic-agent/pkg/control/v2/client" |
| 19 | +) |
| 20 | + |
| 21 | +func newOtelDiagnosticsCommand(streams *cli.IOStreams) *cobra.Command { |
| 22 | + cmd := &cobra.Command{ |
| 23 | + Use: "diagnostics", |
| 24 | + Short: "Gather diagnostics information from the EDOT and write it to a zip archive", |
| 25 | + Long: "This command gathers diagnostics information from the EDOT and writes it to a zip archive", |
| 26 | + RunE: func(cmd *cobra.Command, _ []string) error { |
| 27 | + if err := otelDiagnosticCmd(streams, cmd); err != nil { |
| 28 | + fmt.Fprintf(streams.Err, "Error: %v\n%s\n", err, troubleshootMessage) |
| 29 | + os.Exit(1) |
| 30 | + } |
| 31 | + return nil |
| 32 | + }, |
| 33 | + SilenceUsage: true, |
| 34 | + SilenceErrors: true, |
| 35 | + } |
| 36 | + cmd.Flags().StringP("file", "f", "", "name of the output diagnostics zip archive") |
| 37 | + cmd.Flags().BoolP("cpu-profile", "p", false, "wait to collect a CPU profile") |
| 38 | + return cmd |
| 39 | +} |
| 40 | + |
| 41 | +func otelDiagnosticCmd(streams *cli.IOStreams, cmd *cobra.Command) error { |
| 42 | + cpuProfile, _ := cmd.Flags().GetBool("cpu-profile") |
| 43 | + resp, err := otel.PerformDiagnosticsExt(cmd.Context(), cpuProfile) |
| 44 | + if err != nil { |
| 45 | + return fmt.Errorf("failed to get edot diagnostics: %w", err) |
| 46 | + } |
| 47 | + |
| 48 | + agentDiag := make([]client.DiagnosticFileResult, 0) |
| 49 | + for _, r := range resp.GlobalDiagnostics { |
| 50 | + agentDiag = append(agentDiag, client.DiagnosticFileResult{ |
| 51 | + Name: r.Name, |
| 52 | + Filename: r.Filename, |
| 53 | + ContentType: r.ContentType, |
| 54 | + Content: r.Content, |
| 55 | + Description: r.Description, |
| 56 | + }) |
| 57 | + } |
| 58 | + |
| 59 | + componentDiag := make([]client.DiagnosticComponentResult, 0) |
| 60 | + for _, r := range resp.ComponentDiagnostics { |
| 61 | + res := client.DiagnosticComponentResult{ |
| 62 | + Results: make([]client.DiagnosticFileResult, 0), |
| 63 | + } |
| 64 | + res.Results = append(res.Results, client.DiagnosticFileResult{ |
| 65 | + Name: r.Name, |
| 66 | + Filename: r.Filename, |
| 67 | + ContentType: r.ContentType, |
| 68 | + Content: r.Content, |
| 69 | + Description: r.Description, |
| 70 | + }) |
| 71 | + res.ComponentID = r.Name |
| 72 | + componentDiag = append(componentDiag, res) |
| 73 | + } |
| 74 | + componentDiag = aggregateComponentDiagnostics(componentDiag) |
| 75 | + |
| 76 | + filepath, _ := cmd.Flags().GetString("file") |
| 77 | + if filepath == "" { |
| 78 | + ts := time.Now().UTC() |
| 79 | + filepath = "edot-diagnostics-" + ts.Format("2006-01-02T15-04-05Z07-00") + ".zip" // RFC3339 format that replaces : with -, so it will work on Windows |
| 80 | + } |
| 81 | + f, err := createFile(filepath) |
| 82 | + if err != nil { |
| 83 | + return fmt.Errorf("could not create diagnostics file %q: %w", filepath, err) |
| 84 | + } |
| 85 | + defer f.Close() |
| 86 | + |
| 87 | + // In EDOT, the logs path does not exist, so we ignore that error. |
| 88 | + if err := diagnostics.ZipArchive(streams.Err, f, paths.Top(), agentDiag, nil, componentDiag, false); err != nil && !os.IsNotExist(err) { |
| 89 | + return fmt.Errorf("unable to create archive %q: %w", filepath, err) |
| 90 | + } |
| 91 | + fmt.Fprintf(streams.Out, "Created diagnostics archive %q\n", filepath) |
| 92 | + fmt.Fprintln(streams.Out, "** WARNING **\nCreated archive may contain plain text credentials.\nEnsure that files in archive are redacted before sharing.\n*******") |
| 93 | + return nil |
| 94 | +} |
| 95 | + |
| 96 | +// aggregateComponentDiagnostics takes a slice of DiagnosticComponentResult and merges |
| 97 | +// results for components with the same ComponentID. |
| 98 | +func aggregateComponentDiagnostics(diags []client.DiagnosticComponentResult) []client.DiagnosticComponentResult { |
| 99 | + m := make(map[string]client.DiagnosticComponentResult) |
| 100 | + for _, d := range diags { |
| 101 | + if existing, ok := m[d.ComponentID]; ok { |
| 102 | + existing.Results = append(existing.Results, d.Results...) |
| 103 | + m[d.ComponentID] = existing |
| 104 | + } else { |
| 105 | + m[d.ComponentID] = d |
| 106 | + } |
| 107 | + } |
| 108 | + result := make([]client.DiagnosticComponentResult, 0, len(m)) |
| 109 | + for _, v := range m { |
| 110 | + result = append(result, v) |
| 111 | + } |
| 112 | + return result |
| 113 | +} |
0 commit comments