diff --git a/cmd/openshift-install/analyze.go b/cmd/openshift-install/analyze.go new file mode 100644 index 00000000000..440d25760c2 --- /dev/null +++ b/cmd/openshift-install/analyze.go @@ -0,0 +1,61 @@ +package main + +import ( + "path/filepath" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/openshift/installer/pkg/gather/service" +) + +var ( + analyzeOpts struct { + gatherBundle string + } +) + +func newAnalyzeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "analyze", + Short: "Analyze debugging data for a given installation failure", + Long: `Analyze debugging data for a given installation failure. + +This command helps users to analyze the reasons for an installation that failed while bootstrapping.`, + Args: cobra.ExactArgs(0), + Run: func(_ *cobra.Command, _ []string) { + gatherBundle := analyzeOpts.gatherBundle + if gatherBundle == "" { + var err error + gatherBundle, err = getGatherBundleFromAssetsDirectory() + if err != nil { + logrus.Fatal(err) + } + } + if !filepath.IsAbs(gatherBundle) { + gatherBundle = filepath.Join(rootOpts.dir, gatherBundle) + } + if err := service.AnalyzeGatherBundle(gatherBundle); err != nil { + logrus.Fatal(err) + } + }, + } + cmd.PersistentFlags().StringVar(&analyzeOpts.gatherBundle, "file", "", "Filename of the bootstrap gather bundle; either absolute or relative to the assets directory") + return cmd +} + +func getGatherBundleFromAssetsDirectory() (string, error) { + matches, err := filepath.Glob(filepath.Join(rootOpts.dir, "log-bundle-*.tar.gz")) + if err != nil { + return "", errors.Wrap(err, "could not find gather bundles in assets directory") + } + switch len(matches) { + case 0: + return "", errors.New("no bootstrap gather bundles found in assets directory") + case 1: + return matches[0], nil + default: + return "", errors.New("multiple bootstrap gather bundles found in assets directory; select specific gather bundle by using the --file flag") + } +} diff --git a/cmd/openshift-install/create.go b/cmd/openshift-install/create.go index b2b3c1b8766..23e1852d15a 100644 --- a/cmd/openshift-install/create.go +++ b/cmd/openshift-install/create.go @@ -32,6 +32,7 @@ import ( assetstore "github.com/openshift/installer/pkg/asset/store" targetassets "github.com/openshift/installer/pkg/asset/targets" destroybootstrap "github.com/openshift/installer/pkg/destroy/bootstrap" + "github.com/openshift/installer/pkg/gather/service" timer "github.com/openshift/installer/pkg/metrics/timer" "github.com/openshift/installer/pkg/types/baremetal" cov1helpers "github.com/openshift/library-go/pkg/config/clusteroperator/v1helpers" @@ -115,11 +116,15 @@ var ( if err2 := logClusterOperatorConditions(ctx, config); err2 != nil { logrus.Error("Attempted to gather ClusterOperator status after installation failure: ", err2) } - if err2 := runGatherBootstrapCmd(rootOpts.dir); err2 != nil { + bundlePath, err2 := runGatherBootstrapCmd(rootOpts.dir) + if err2 != nil { logrus.Error("Attempted to gather debug logs after installation failure: ", err2) } logrus.Error("Bootstrap failed to complete: ", err.Unwrap()) logrus.Error(err.Error()) + if err2 := service.AnalyzeGatherBundle(bundlePath); err2 != nil { + logrus.Error("Attempted to analyze the debug logs after installation failure: ", err2) + } logrus.Fatal("Bootstrap failed to complete") } timer.StopTimer("Bootstrap Complete") diff --git a/cmd/openshift-install/gather.go b/cmd/openshift-install/gather.go index c242803be95..fadfcaf6894 100644 --- a/cmd/openshift-install/gather.go +++ b/cmd/openshift-install/gather.go @@ -23,6 +23,7 @@ import ( "github.com/openshift/installer/pkg/asset/installconfig" assetstore "github.com/openshift/installer/pkg/asset/store" "github.com/openshift/installer/pkg/asset/tls" + "github.com/openshift/installer/pkg/gather/service" "github.com/openshift/installer/pkg/gather/ssh" "github.com/openshift/installer/pkg/terraform" gatheraws "github.com/openshift/installer/pkg/terraform/gather/aws" @@ -52,7 +53,7 @@ func newGatherCmd() *cobra.Command { Short: "Gather debugging data for a given installation failure", Long: `Gather debugging data for a given installation failure. -When installation for Openshift cluster fails, gathering all the data useful for debugging can +When installation for OpenShift cluster fails, gathering all the data useful for debugging can become a difficult task. This command helps users to collect the most relevant information that can be used to debug the installation failures`, RunE: func(cmd *cobra.Command, args []string) error { @@ -65,9 +66,10 @@ to debug the installation failures`, var ( gatherBootstrapOpts struct { - bootstrap string - masters []string - sshKeys []string + bootstrap string + masters []string + sshKeys []string + skipAnalysis bool } ) @@ -79,38 +81,44 @@ func newGatherBootstrapCmd() *cobra.Command { Run: func(_ *cobra.Command, _ []string) { cleanup := setupFileHook(rootOpts.dir) defer cleanup() - err := runGatherBootstrapCmd(rootOpts.dir) + bundlePath, err := runGatherBootstrapCmd(rootOpts.dir) if err != nil { logrus.Fatal(err) } + if !gatherBootstrapOpts.skipAnalysis { + if err := service.AnalyzeGatherBundle(bundlePath); err != nil { + logrus.Fatal(err) + } + } }, } cmd.PersistentFlags().StringVar(&gatherBootstrapOpts.bootstrap, "bootstrap", "", "Hostname or IP of the bootstrap host") cmd.PersistentFlags().StringArrayVar(&gatherBootstrapOpts.masters, "master", []string{}, "Hostnames or IPs of all control plane hosts") cmd.PersistentFlags().StringArrayVar(&gatherBootstrapOpts.sshKeys, "key", []string{}, "Path to SSH private keys that should be used for authentication. If no key was provided, SSH private keys from user's environment will be used") + cmd.PersistentFlags().BoolVar(&gatherBootstrapOpts.skipAnalysis, "skipAnalysis", false, "Skip analysis of the gathered data") return cmd } -func runGatherBootstrapCmd(directory string) error { +func runGatherBootstrapCmd(directory string) (string, error) { assetStore, err := assetstore.NewStore(directory) if err != nil { - return errors.Wrap(err, "failed to create asset store") + return "", errors.Wrap(err, "failed to create asset store") } // add the default bootstrap key pair to the sshKeys list bootstrapSSHKeyPair := &tls.BootstrapSSHKeyPair{} if err := assetStore.Fetch(bootstrapSSHKeyPair); err != nil { - return errors.Wrapf(err, "failed to fetch %s", bootstrapSSHKeyPair.Name()) + return "", errors.Wrapf(err, "failed to fetch %s", bootstrapSSHKeyPair.Name()) } tmpfile, err := ioutil.TempFile("", "bootstrap-ssh") if err != nil { - return err + return "", err } defer os.Remove(tmpfile.Name()) if _, err := tmpfile.Write(bootstrapSSHKeyPair.Private()); err != nil { - return err + return "", err } if err := tmpfile.Close(); err != nil { - return err + return "", err } gatherBootstrapOpts.sshKeys = append(gatherBootstrapOpts.sshKeys, tmpfile.Name()) @@ -120,17 +128,17 @@ func runGatherBootstrapCmd(directory string) error { return unSupportedPlatformGather(directory) } if err != nil { - return err + return "", err } config := &installconfig.InstallConfig{} if err := assetStore.Fetch(config); err != nil { - return errors.Wrapf(err, "failed to fetch %s", config.Name()) + return "", errors.Wrapf(err, "failed to fetch %s", config.Name()) } tfstate, err := terraform.ReadState(tfStateFilePath) if err != nil { - return errors.Wrapf(err, "failed to read state from %q", tfStateFilePath) + return "", errors.Wrapf(err, "failed to read state from %q", tfStateFilePath) } bootstrap, port, masters, err := extractHostAddresses(config.Config, tfstate) if err != nil { @@ -138,36 +146,36 @@ func runGatherBootstrapCmd(directory string) error { logrus.Error(err2) return unSupportedPlatformGather(directory) } - return errors.Wrapf(err, "failed to get bootstrap and control plane host addresses from %q", tfStateFilePath) + return "", errors.Wrapf(err, "failed to get bootstrap and control plane host addresses from %q", tfStateFilePath) } return logGatherBootstrap(bootstrap, port, masters, directory) } -func logGatherBootstrap(bootstrap string, port int, masters []string, directory string) error { +func logGatherBootstrap(bootstrap string, port int, masters []string, directory string) (string, error) { logrus.Info("Pulling debug logs from the bootstrap machine") client, err := ssh.NewClient("core", net.JoinHostPort(bootstrap, strconv.Itoa(port)), gatherBootstrapOpts.sshKeys) if err != nil { if errors.Is(err, syscall.ECONNREFUSED) { - return errors.Wrap(err, "failed to connect to the bootstrap machine") + return "", errors.Wrap(err, "failed to connect to the bootstrap machine") } - return errors.Wrap(err, "failed to create SSH client") + return "", errors.Wrap(err, "failed to create SSH client") } gatherID := time.Now().Format("20060102150405") if err := ssh.Run(client, fmt.Sprintf("/usr/local/bin/installer-gather.sh --id %s %s", gatherID, strings.Join(masters, " "))); err != nil { - return errors.Wrap(err, "failed to run remote command") + return "", errors.Wrap(err, "failed to run remote command") } file := filepath.Join(directory, fmt.Sprintf("log-bundle-%s.tar.gz", gatherID)) if err := ssh.PullFileTo(client, fmt.Sprintf("/home/core/log-bundle-%s.tar.gz", gatherID), file); err != nil { - return errors.Wrap(err, "failed to pull log file from remote") + return "", errors.Wrap(err, "failed to pull log file from remote") } path, err := filepath.Abs(file) if err != nil { - return errors.Wrap(err, "failed to stat log file") + return "", errors.Wrap(err, "failed to stat log file") } logrus.Infof("Bootstrap gather logs captured here %q", path) - return nil + return path, nil } func extractHostAddresses(config *types.InstallConfig, tfstate *terraform.State) (bootstrap string, port int, masters []string, err error) { @@ -266,9 +274,9 @@ func (e errUnSupportedGatherPlatform) Error() string { return e.Message } -func unSupportedPlatformGather(directory string) error { +func unSupportedPlatformGather(directory string) (string, error) { if gatherBootstrapOpts.bootstrap == "" || len(gatherBootstrapOpts.masters) == 0 { - return errors.New("bootstrap host address and at least one control plane host address must be provided") + return "", errors.New("bootstrap host address and at least one control plane host address must be provided") } return logGatherBootstrap(gatherBootstrapOpts.bootstrap, 22, gatherBootstrapOpts.masters, directory) diff --git a/cmd/openshift-install/main.go b/cmd/openshift-install/main.go index a7699b6d5f5..c49cbf2d553 100644 --- a/cmd/openshift-install/main.go +++ b/cmd/openshift-install/main.go @@ -58,6 +58,7 @@ func installerMain() { newDestroyCmd(), newWaitForCmd(), newGatherCmd(), + newAnalyzeCmd(), newVersionCmd(), newGraphCmd(), newCoreOSCmd(), diff --git a/pkg/gather/service/analyze.go b/pkg/gather/service/analyze.go new file mode 100644 index 00000000000..3932d01747b --- /dev/null +++ b/pkg/gather/service/analyze.go @@ -0,0 +1,160 @@ +package service + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "io" + "os" + "regexp" + "strings" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// regex matching the path of a service entries file. The captured group is the name of the service. +// For example, if the filename is "log-bundle-20210329190553/bootstrap/services/release-image.json", +// then the name of the service is "release-image". +var serviceEntriesFilePathRegex = regexp.MustCompile(`^[^\/]+\/bootstrap\/services\/([^.]+)\.json$`) + +// AnalyzeGatherBundle will analyze the bootstrap gather bundle at the specified path. +// Analysis will be logged. +// Returns an error if there was a problem reading the bundle. +func AnalyzeGatherBundle(bundlePath string) error { + // open the bundle file for reading + bundleFile, err := os.Open(bundlePath) + if err != nil { + return errors.Wrap(err, "could not open the gather bundle") + } + defer bundleFile.Close() + return analyzeGatherBundle(bundleFile) +} + +func analyzeGatherBundle(bundleFile io.Reader) error { + // decompress the bundle + uncompressedStream, err := gzip.NewReader(bundleFile) + if err != nil { + return errors.Wrap(err, "could not decompress the gather bundle") + } + defer uncompressedStream.Close() + + // read through the tar for relevant files + tarReader := tar.NewReader(uncompressedStream) + serviceAnalyses := make(map[string]analysis) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return errors.Wrap(err, "encountered an error reading from the gather bundle") + } + if header.Typeflag != tar.TypeReg { + continue + } + + serviceEntriesFileSubmatch := serviceEntriesFilePathRegex.FindStringSubmatch(header.Name) + if serviceEntriesFileSubmatch == nil { + continue + } + serviceName := serviceEntriesFileSubmatch[1] + + serviceAnalysis, err := analyzeService(tarReader) + if err != nil { + logrus.Infof("Could not analyze the %s.service: %v", serviceName, err) + continue + } + + serviceAnalyses[serviceName] = serviceAnalysis + } + + analysisChecks := []struct { + name string + check func(analysis) bool + }{ + {name: "release-image", check: checkReleaseImageDownload}, + } + for _, check := range analysisChecks { + a := serviceAnalyses[check.name] + if a.starts == 0 { + logrus.Errorf("The bootstrap machine did not execute the %s.service systemd unit", check.name) + break + } + if !check.check(a) { + break + } + } + + return nil +} + +func checkReleaseImageDownload(a analysis) bool { + if a.successful { + return true + } + logrus.Error("The bootstrap machine failed to download the release image") + a.logLastError() + return false +} + +type analysis struct { + // starts is the number of times that the service started + starts int + // successful is true if the last invocation of the service ended in success + successful bool + // failingStage is the stage that failed in the last unsuccessful invocation of the service + failingStage string + // lastError is the last error recorded in the last failure of the service + lastError string +} + +func analyzeService(r io.Reader) (analysis, error) { + a := analysis{} + decoder := json.NewDecoder(r) + t, err := decoder.Token() + if err != nil { + return a, errors.Wrap(err, "service entries file does not begin with a token") + } + delim, isDelim := t.(json.Delim) + if !isDelim { + return a, errors.New("service entries file does not begin with a delimiter") + } + if delim != '[' { + return a, errors.New("service entries file does not begin with an array") + } + var lastEntry *Entry + for decoder.More() { + entry := &Entry{} + if err := decoder.Decode(entry); err != nil { + return a, errors.Wrap(err, "could not decode an entry in the service entries file") + } + + // record a new start of the service + if entry.Phase == ServiceStart { + a.starts++ + } + + // the service is only considered successful if the last entry is either the service ending successfully or a + // post-command ending successfully. + a.successful = entry.Result == Success && (entry.Phase == ServiceEnd || entry.Phase == PostCommandEnd) + + // save the last error + if entry.Result == Failure { + // if a stage failure causes a service (or pre- or post-command) failure, we want to preserve the failing + // stage from the stage end entry. + if lastEntry == nil || lastEntry.Phase != StageEnd || lastEntry.Result != Failure { + a.failingStage = entry.Stage + } + a.lastError = entry.ErrorMessage + } + lastEntry = entry + } + return a, nil +} + +func (a analysis) logLastError() { + for _, l := range strings.Split(a.lastError, "\n") { + logrus.Info(l) + } +} diff --git a/pkg/gather/service/analyze_test.go b/pkg/gather/service/analyze_test.go new file mode 100644 index 00000000000..b837564c1fc --- /dev/null +++ b/pkg/gather/service/analyze_test.go @@ -0,0 +1,117 @@ +package service + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "testing" + + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" +) + +func TestAnalyzeGatherBundle(t *testing.T) { + cases := []struct { + name string + files map[string]string + expectedOutput []logrus.Entry + }{ + { + name: "no files", + expectedOutput: []logrus.Entry{ + {Level: logrus.ErrorLevel, Message: "The bootstrap machine did not execute the release-image.service systemd unit"}, + }, + }, + { + name: "release-image not started", + files: map[string]string{ + "log-bundle/bootstrap/services/release-image.json": "[]", + }, + expectedOutput: []logrus.Entry{ + {Level: logrus.ErrorLevel, Message: "The bootstrap machine did not execute the release-image.service systemd unit"}, + }, + }, + { + name: "release-image successful", + files: map[string]string{ + "log-bundle/bootstrap/services/release-image.json": `[ +{"phase":"service start"}, +{"phase":"stage start", "stage":"pull-release-image"}, +{"phase":"stage end", "stage":"pull-release-image", "result":"success"}, +{"phase":"service end", "result":"success"} +]`, + }, + }, + { + name: "release-image failed", + files: map[string]string{ + "log-bundle/bootstrap/services/release-image.json": `[ +{"phase":"service start"}, +{"phase":"stage start", "stage":"pull-release-image"}, +{"phase":"stage end", "stage":"pull-release-image", "result":"failure", "errorMessage":"Line 1\nLine 2\nLine 3"} +]`, + }, + expectedOutput: []logrus.Entry{ + {Level: logrus.ErrorLevel, Message: "The bootstrap machine failed to download the release image"}, + {Level: logrus.InfoLevel, Message: "Line 1"}, + {Level: logrus.InfoLevel, Message: "Line 2"}, + {Level: logrus.InfoLevel, Message: "Line 3"}, + }, + }, + { + name: "empty release-image.json", + files: map[string]string{ + "log-bundle/bootstrap/services/release-image.json": "", + }, + expectedOutput: []logrus.Entry{ + {Level: logrus.InfoLevel, Message: "Could not analyze the release-image.service: service entries file does not begin with a token: EOF"}, + {Level: logrus.ErrorLevel, Message: "The bootstrap machine did not execute the release-image.service systemd unit"}, + }, + }, + { + name: "malformed release-image.json", + files: map[string]string{ + "log-bundle/bootstrap/services/release-image.json": "{}", + }, + expectedOutput: []logrus.Entry{ + {Level: logrus.InfoLevel, Message: "Could not analyze the release-image.service: service entries file does not begin with an array"}, + {Level: logrus.ErrorLevel, Message: "The bootstrap machine did not execute the release-image.service systemd unit"}, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var gatherBuilder bytes.Buffer + gzipWriter := gzip.NewWriter(&gatherBuilder) + defer gzipWriter.Close() + tarWriter := tar.NewWriter(gzipWriter) + defer tarWriter.Close() + for filename, contents := range tc.files { + contentsAsBytes := []byte(contents) + if err := tarWriter.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: filename, + Size: int64(len(contentsAsBytes)), + }); err != nil { + t.Fatal(err) + } + if _, err := tarWriter.Write(contentsAsBytes); err != nil { + t.Fatal(err) + } + } + gzipWriter.Close() + hook := test.NewLocal(logrus.StandardLogger()) + defer hook.Reset() + err := analyzeGatherBundle(&gatherBuilder) + assert.NoError(t, err, "unexpected error from analysis") + for i, e := range hook.Entries { + hook.Entries[i] = logrus.Entry{ + Level: e.Level, + Message: e.Message, + } + } + assert.Equal(t, tc.expectedOutput, hook.Entries) + }) + } +} diff --git a/pkg/gather/service/doc.go b/pkg/gather/service/doc.go new file mode 100644 index 00000000000..901172acc6f --- /dev/null +++ b/pkg/gather/service/doc.go @@ -0,0 +1,2 @@ +// Package service is used to analyze service json files from an installation that failing to bootstrap. +package service diff --git a/pkg/gather/service/entry.go b/pkg/gather/service/entry.go new file mode 100644 index 00000000000..0fda98f1ee1 --- /dev/null +++ b/pkg/gather/service/entry.go @@ -0,0 +1,58 @@ +package service + +// Entry is an entry in a service entries file +type Entry struct { + // Timestamp is the time at which the entry was recorded + Timestamp string `json:"timestamp"` + // Phase is the phase of the service + Phase Phase `json:"phase"` + // Result is the result of either the service, the stage, the pre-command, or the post-command. This is only + // present when the phase is an ending phase. + Result Result `json:"result,omitempty"` + // Stage is the name of the stage being executed. This is only present when the phase is either StageStart or StageEnd. + Stage string `json:"string,omitempty"` + // PreCommand is the name of the pre-command being executed. This is only present when the phase is either + // PreCommandStart or PreCommandEnd. + PreCommand string `json:"preCommand,omitempty"` + // PostCommand is the name of the post-command being executed. This is only present when the phase is either + // PostCommandStart or PostCommandEnd. + PostCommand string `json:"postCommand,omitempty"` + // ErrorLine is the location where the error occurred that caused the failure. This is only present when the result + // is Failure. + ErrorLine string `json:"errorLine,omitempty"` + // ErrorMessage is the last few output messages from the service prior to the error. This is only present when the + // result is Failure. + ErrorMessage string `json:"errorMessage,omitempty"` +} + +// Phase is the phase of the service. +type Phase string + +const ( + // ServiceStart is the phase when the main command of the service starts. + ServiceStart Phase = "service start" + // ServiceEnd is the phase when the main command of the service ends. + ServiceEnd Phase = "service end" + // StageStart is the phase when a stage of the service starts. + StageStart Phase = "stage start" + // StageEnd is the phase when a stage of the service ends. + StageEnd Phase = "stage end" + // PreCommandStart is the phase when a pre-command of the service starts. + PreCommandStart Phase = "pre-command start" + // PreCommandEnd is the phase when a pre-command of the service ends. + PreCommandEnd Phase = "pre-command end" + // PostCommandStart is the phase when a post-command of the service starts. + PostCommandStart Phase = "post-command start" + // PostCommandEnd is the phase when a post-command of the service ends. + PostCommandEnd Phase = "post-command end" +) + +// Result is the result of either the service, the stage, the pre-command, or the post-command. +type Result string + +const ( + // Success indicates that the service, stage, pre-command, or post-command was successful. + Success Result = "success" + // Failure indicates that the service, stage, pre-command, or post-command ended due to a failure. + Failure Result = "failure" +) diff --git a/vendor/github.com/sirupsen/logrus/hooks/test/test.go b/vendor/github.com/sirupsen/logrus/hooks/test/test.go new file mode 100644 index 00000000000..b16d06654ae --- /dev/null +++ b/vendor/github.com/sirupsen/logrus/hooks/test/test.go @@ -0,0 +1,91 @@ +// The Test package is used for testing logrus. +// It provides a simple hooks which register logged messages. +package test + +import ( + "io/ioutil" + "sync" + + "github.com/sirupsen/logrus" +) + +// Hook is a hook designed for dealing with logs in test scenarios. +type Hook struct { + // Entries is an array of all entries that have been received by this hook. + // For safe access, use the AllEntries() method, rather than reading this + // value directly. + Entries []logrus.Entry + mu sync.RWMutex +} + +// NewGlobal installs a test hook for the global logger. +func NewGlobal() *Hook { + + hook := new(Hook) + logrus.AddHook(hook) + + return hook + +} + +// NewLocal installs a test hook for a given local logger. +func NewLocal(logger *logrus.Logger) *Hook { + + hook := new(Hook) + logger.Hooks.Add(hook) + + return hook + +} + +// NewNullLogger creates a discarding logger and installs the test hook. +func NewNullLogger() (*logrus.Logger, *Hook) { + + logger := logrus.New() + logger.Out = ioutil.Discard + + return logger, NewLocal(logger) + +} + +func (t *Hook) Fire(e *logrus.Entry) error { + t.mu.Lock() + defer t.mu.Unlock() + t.Entries = append(t.Entries, *e) + return nil +} + +func (t *Hook) Levels() []logrus.Level { + return logrus.AllLevels +} + +// LastEntry returns the last entry that was logged or nil. +func (t *Hook) LastEntry() *logrus.Entry { + t.mu.RLock() + defer t.mu.RUnlock() + i := len(t.Entries) - 1 + if i < 0 { + return nil + } + return &t.Entries[i] +} + +// AllEntries returns all entries that were logged. +func (t *Hook) AllEntries() []*logrus.Entry { + t.mu.RLock() + defer t.mu.RUnlock() + // Make a copy so the returned value won't race with future log requests + entries := make([]*logrus.Entry, len(t.Entries)) + for i := 0; i < len(t.Entries); i++ { + // Make a copy, for safety + entries[i] = &t.Entries[i] + } + return entries +} + +// Reset removes all Entries from this test hook. +func (t *Hook) Reset() { + t.mu.Lock() + defer t.mu.Unlock() + t.Entries = make([]logrus.Entry, 0) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2ac77023214..2f3a1de7405 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1219,6 +1219,7 @@ github.com/shurcooL/vfsgen # github.com/sirupsen/logrus v1.7.0 ## explicit github.com/sirupsen/logrus +github.com/sirupsen/logrus/hooks/test # github.com/spf13/afero v1.3.5 github.com/spf13/afero github.com/spf13/afero/mem