istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/bug-report/pkg/bugreport/bugreport.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package bugreport
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"path"
    22  	"path/filepath"
    23  	"reflect"
    24  	"runtime"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  
    29  	"github.com/kr/pretty"
    30  	"github.com/spf13/cobra"
    31  
    32  	label2 "istio.io/api/label"
    33  	"istio.io/istio/istioctl/pkg/cli"
    34  	"istio.io/istio/istioctl/pkg/util/ambient"
    35  	"istio.io/istio/operator/pkg/util"
    36  	"istio.io/istio/pkg/kube"
    37  	"istio.io/istio/pkg/kube/inject"
    38  	"istio.io/istio/pkg/log"
    39  	"istio.io/istio/pkg/proxy"
    40  	"istio.io/istio/pkg/util/sets"
    41  	"istio.io/istio/pkg/version"
    42  	"istio.io/istio/tools/bug-report/pkg/archive"
    43  	cluster2 "istio.io/istio/tools/bug-report/pkg/cluster"
    44  	"istio.io/istio/tools/bug-report/pkg/common"
    45  	"istio.io/istio/tools/bug-report/pkg/config"
    46  	"istio.io/istio/tools/bug-report/pkg/content"
    47  	"istio.io/istio/tools/bug-report/pkg/filter"
    48  	"istio.io/istio/tools/bug-report/pkg/kubeclient"
    49  	"istio.io/istio/tools/bug-report/pkg/kubectlcmd"
    50  	"istio.io/istio/tools/bug-report/pkg/processlog"
    51  )
    52  
    53  const (
    54  	bugReportDefaultTimeout = 30 * time.Minute
    55  )
    56  
    57  var (
    58  	bugReportDefaultIstioNamespace = "istio-system"
    59  	bugReportDefaultInclude        = []string{""}
    60  	bugReportDefaultExclude        = []string{strings.Join(sets.SortedList(inject.IgnoredNamespaces), ",")}
    61  )
    62  
    63  // Cmd returns a cobra command for bug-report.
    64  func Cmd(ctx cli.Context, logOpts *log.Options) *cobra.Command {
    65  	rootCmd := &cobra.Command{
    66  		Use:          "bug-report",
    67  		Short:        "Cluster information and log capture support tool.",
    68  		SilenceUsage: true,
    69  		Long: `bug-report selectively captures cluster information and logs into an archive to help diagnose problems.
    70  Proxy logs can be filtered using:
    71    --include|--exclude ns1,ns2.../dep1,dep2.../pod1,pod2.../lbl1=val1,lbl2=val2.../ann1=val1,ann2=val2.../cntr1,cntr...
    72  where ns=namespace, dep=deployment, lbl=label, ann=annotation, cntr=container
    73  
    74  The filter spec is interpreted as 'must be in (ns1 OR ns2) AND (dep1 OR dep2) AND (cntr1 OR cntr2)...'
    75  The log will be included only if the container matches at least one include filter and does not match any exclude filters.
    76  All parts of the filter are optional and can be omitted e.g. ns1//pod1 filters only for namespace ns1 and pod1.
    77  All names except label and annotation keys support '*' glob matching pattern.
    78  
    79  e.g.
    80  --include ns1,ns2 (only namespaces ns1 and ns2)
    81  --include n*//p*/l=v* (pods with name beginning with 'p' in namespaces beginning with 'n' and having label 'l' with value beginning with 'v'.)`,
    82  		RunE: func(cmd *cobra.Command, args []string) error {
    83  			return runBugReportCommand(ctx, cmd, logOpts)
    84  		},
    85  	}
    86  	rootCmd.AddCommand(version.CobraCommand())
    87  	addFlags(rootCmd, gConfig)
    88  
    89  	return rootCmd
    90  }
    91  
    92  var (
    93  	// Logs, along with stats and importance metrics. Key is path (namespace/deployment/pod/cluster) which can be
    94  	// parsed with ParsePath.
    95  	logs       = make(map[string]string)
    96  	stats      = make(map[string]*processlog.Stats)
    97  	importance = make(map[string]int)
    98  	// Aggregated errors for all fetch operations.
    99  	gErrors util.Errors
   100  	lock    = sync.RWMutex{}
   101  )
   102  
   103  func runBugReportCommand(ctx cli.Context, _ *cobra.Command, logOpts *log.Options) error {
   104  	runner := kubectlcmd.NewRunner(gConfig.RequestConcurrency)
   105  	runner.ReportRunningTasks()
   106  	if err := configLogs(logOpts); err != nil {
   107  		return err
   108  	}
   109  	config, err := parseConfig()
   110  	if err != nil {
   111  		return err
   112  	}
   113  	clusterCtxStr := ""
   114  	if config.Context == "" {
   115  		var err error
   116  		clusterCtxStr, err = content.GetClusterContext(runner, config.KubeConfigPath)
   117  		if err != nil {
   118  			return err
   119  		}
   120  	} else {
   121  		clusterCtxStr = config.Context
   122  	}
   123  
   124  	common.LogAndPrintf("\nTarget cluster context: %s\n", clusterCtxStr)
   125  	common.LogAndPrintf("Running with the following config: \n\n%s\n\n", config)
   126  
   127  	restConfig, clientset, err := kubeclient.New(config.KubeConfigPath, config.Context)
   128  	if err != nil {
   129  		return fmt.Errorf("could not initialize k8s client: %s ", err)
   130  	}
   131  	client, err := kube.NewCLIClient(kube.NewClientConfigForRestConfig(restConfig))
   132  	if err != nil {
   133  		return err
   134  	}
   135  	common.LogAndPrintf("\nCluster endpoint: %s\n", client.RESTConfig().Host)
   136  	runner.SetClient(client)
   137  
   138  	clusterResourcesCtx, getClusterResourcesCancel := context.WithTimeout(context.Background(), commandTimeout)
   139  	curTime := time.Now()
   140  	defer func() {
   141  		if time.Until(curTime.Add(commandTimeout)) < 0 {
   142  			message := "Timeout when running bug report command, please using --include or --exclude to filter"
   143  			common.LogAndPrintf(message)
   144  		}
   145  		getClusterResourcesCancel()
   146  	}()
   147  	resources, err := cluster2.GetClusterResources(clusterResourcesCtx, clientset, config)
   148  	if err != nil {
   149  		return err
   150  	}
   151  	logRuntime(curTime, "Done collecting cluster resource")
   152  
   153  	dumpRevisionsAndVersions(ctx, resources, config.IstioNamespace, config.DryRun)
   154  
   155  	log.Infof("Cluster resource tree:\n\n%s\n\n", resources)
   156  	paths, err := filter.GetMatchingPaths(config, resources)
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	common.LogAndPrintf("\n\nFetching logs for the following containers:\n\n%s\n", strings.Join(paths, "\n"))
   162  
   163  	gatherInfo(runner, config, resources, paths)
   164  	if len(gErrors) != 0 {
   165  		log.Error(gErrors.ToError())
   166  	}
   167  
   168  	// TODO: sort by importance and discard any over the size limit.
   169  	for path, text := range logs {
   170  		namespace, _, pod, _, err := cluster2.ParsePath(path)
   171  		if err != nil {
   172  			log.Errorf(err.Error())
   173  			continue
   174  		}
   175  		writeFile(filepath.Join(archive.ProxyOutputPath(tempDir, namespace, pod), common.ProxyContainerName+".log"), text, config.DryRun)
   176  	}
   177  
   178  	logRuntime(curTime, "Done with bug-report command before generating the archive file")
   179  
   180  	outDir, err := os.Getwd()
   181  	if err != nil {
   182  		log.Errorf("using ./ to write archive: %s", err.Error())
   183  		outDir = "."
   184  	}
   185  	if outputDir != "" {
   186  		outDir = outputDir
   187  	}
   188  	outPath := filepath.Join(outDir, "bug-report.tar.gz")
   189  
   190  	if !config.DryRun {
   191  		common.LogAndPrintf("Creating an archive at %s.\n", outPath)
   192  		archiveDir := archive.DirToArchive(tempDir)
   193  		if tempDir != "" {
   194  			archiveDir = tempDir
   195  		}
   196  		curTime = time.Now()
   197  		err := archive.Create(archiveDir, outPath)
   198  		fmt.Printf("Time used for creating the tar file is %v.\n", time.Since(curTime))
   199  		if err != nil {
   200  			return err
   201  		}
   202  		common.LogAndPrintf("Cleaning up temporary files in %s.\n", archiveDir)
   203  		if err := os.RemoveAll(archiveDir); err != nil {
   204  			return err
   205  		}
   206  	} else {
   207  		common.LogAndPrintf("Dry run, skipping archive creation at %s.\n", outPath)
   208  	}
   209  	common.LogAndPrintf("Done.\n")
   210  	return nil
   211  }
   212  
   213  func dumpRevisionsAndVersions(ctx cli.Context, resources *cluster2.Resources, istioNamespace string, dryRun bool) {
   214  	defer logRuntime(time.Now(), "Done getting control plane revisions/versions")
   215  
   216  	text := ""
   217  	text += fmt.Sprintf("CLI version:\n%s\n\n", version.Info.LongForm())
   218  
   219  	revisions := getIstioRevisions(resources)
   220  	istioVersions, proxyVersions := getIstioVersions(ctx, istioNamespace, revisions)
   221  	text += "The following Istio control plane revisions/versions were found in the cluster:\n"
   222  	for rev, ver := range istioVersions {
   223  		text += fmt.Sprintf("Revision %s:\n%s\n\n", rev, ver)
   224  	}
   225  	text += "The following proxy revisions/versions were found in the cluster:\n"
   226  	for rev, ver := range proxyVersions {
   227  		text += fmt.Sprintf("Revision %s: Versions {%s}\n", rev, strings.Join(ver, ", "))
   228  	}
   229  	common.LogAndPrintf(text)
   230  	writeFile(filepath.Join(archive.OutputRootDir(tempDir), "versions"), text, dryRun)
   231  }
   232  
   233  // getIstioRevisions returns a slice with all Istio revisions detected in the cluster.
   234  func getIstioRevisions(resources *cluster2.Resources) []string {
   235  	revMap := sets.New[string]()
   236  	for _, podLabels := range resources.Labels {
   237  		for label, value := range podLabels {
   238  			if label == label2.IoIstioRev.Name {
   239  				revMap.Insert(value)
   240  			}
   241  		}
   242  	}
   243  	for _, podAnnotations := range resources.Annotations {
   244  		for annotation, value := range podAnnotations {
   245  			if annotation == label2.IoIstioRev.Name {
   246  				revMap.Insert(value)
   247  			}
   248  		}
   249  	}
   250  	return sets.SortedList(revMap)
   251  }
   252  
   253  // getIstioVersions returns a mapping of revision to aggregated version string for Istio components and revision to
   254  // slice of versions for proxies. Any errors are embedded in the revision strings.
   255  func getIstioVersions(ctx cli.Context, istioNamespace string, revisions []string) (map[string]string, map[string][]string) {
   256  	istioVersions := make(map[string]string)
   257  	proxyVersionsMap := make(map[string]sets.String)
   258  	proxyVersions := make(map[string][]string)
   259  	for _, revision := range revisions {
   260  		client, err := ctx.CLIClientWithRevision(revision)
   261  		if err != nil {
   262  			log.Error(err)
   263  			continue
   264  		}
   265  		istioVersions[revision] = getIstioVersion(client, istioNamespace)
   266  		proxyInfo, err := proxy.GetProxyInfo(client, istioNamespace)
   267  		if err != nil {
   268  			log.Error(err)
   269  			continue
   270  		}
   271  		for _, pi := range *proxyInfo {
   272  			sets.InsertOrNew(proxyVersionsMap, revision, pi.IstioVersion)
   273  		}
   274  	}
   275  	for revision, vmap := range proxyVersionsMap {
   276  		for v := range vmap {
   277  			proxyVersions[revision] = append(proxyVersions[revision], v)
   278  		}
   279  	}
   280  	return istioVersions, proxyVersions
   281  }
   282  
   283  func getIstioVersion(kubeClient kube.CLIClient, istioNamespace string) string {
   284  	versions, err := kubeClient.GetIstioVersions(context.TODO(), istioNamespace)
   285  	if err != nil {
   286  		return err.Error()
   287  	}
   288  	return pretty.Sprint(versions)
   289  }
   290  
   291  // gatherInfo fetches all logs, resources, debug etc. using goroutines.
   292  // proxy logs and info are saved in logs/stats/importance global maps.
   293  // Errors are reported through gErrors.
   294  func gatherInfo(runner *kubectlcmd.Runner, config *config.BugReportConfig, resources *cluster2.Resources, paths []string) {
   295  	// no timeout on mandatoryWg.
   296  	var mandatoryWg sync.WaitGroup
   297  	cmdTimer := time.NewTimer(time.Duration(config.CommandTimeout))
   298  	beginTime := time.Now()
   299  
   300  	client, err := kube.NewCLIClient(kube.BuildClientCmd(config.KubeConfigPath, config.Context))
   301  	if err != nil {
   302  		appendGlobalErr(err)
   303  	}
   304  
   305  	clusterDir := archive.ClusterInfoPath(tempDir)
   306  
   307  	params := &content.Params{
   308  		Runner:      runner,
   309  		DryRun:      config.DryRun,
   310  		KubeConfig:  config.KubeConfigPath,
   311  		KubeContext: config.Context,
   312  	}
   313  	common.LogAndPrintf("\nFetching Istio control plane information from cluster.\n\n")
   314  	getFromCluster(content.GetK8sResources, params, clusterDir, &mandatoryWg)
   315  	getFromCluster(content.GetCRs, params, clusterDir, &mandatoryWg)
   316  	getFromCluster(content.GetEvents, params, clusterDir, &mandatoryWg)
   317  	getFromCluster(content.GetClusterInfo, params, clusterDir, &mandatoryWg)
   318  	getFromCluster(content.GetNodeInfo, params, clusterDir, &mandatoryWg)
   319  	getFromCluster(content.GetSecrets, params.SetVerbose(config.FullSecrets), clusterDir, &mandatoryWg)
   320  	getFromCluster(content.GetPodInfo, params.SetIstioNamespace(config.IstioNamespace), clusterDir, &mandatoryWg)
   321  
   322  	common.LogAndPrintf("\nFetching CNI logs from cluster.\n\n")
   323  	for _, cniPod := range resources.CniPod {
   324  		getCniLogs(runner, config, resources, cniPod.Namespace, cniPod.Name, &mandatoryWg)
   325  	}
   326  
   327  	// optionalWg is subject to timer.
   328  	var optionalWg sync.WaitGroup
   329  	for _, p := range paths {
   330  		namespace, _, pod, container, err := cluster2.ParsePath(p)
   331  		if err != nil {
   332  			log.Error(err.Error())
   333  			continue
   334  		}
   335  
   336  		cp := params.SetNamespace(namespace).SetPod(pod).SetContainer(container)
   337  		proxyDir := archive.ProxyOutputPath(tempDir, namespace, pod)
   338  		switch {
   339  		case common.IsProxyContainer(params.ClusterVersion, container):
   340  			if !ambient.IsZtunnelPod(client, pod, namespace) {
   341  				getFromCluster(content.GetCoredumps, cp, filepath.Join(proxyDir, "cores"), &mandatoryWg)
   342  				getFromCluster(content.GetNetstat, cp, proxyDir, &mandatoryWg)
   343  				getFromCluster(content.GetProxyInfo, cp, archive.ProxyOutputPath(tempDir, namespace, pod), &optionalWg)
   344  				getProxyLogs(runner, config, resources, p, namespace, pod, container, &optionalWg)
   345  			} else {
   346  				getFromCluster(content.GetNetstat, cp, proxyDir, &mandatoryWg)
   347  				getFromCluster(content.GetZtunnelInfo, cp, archive.ProxyOutputPath(tempDir, namespace, pod), &optionalWg)
   348  				getProxyLogs(runner, config, resources, p, namespace, pod, container, &optionalWg)
   349  			}
   350  		case resources.IsDiscoveryContainer(params.ClusterVersion, namespace, pod, container):
   351  			getFromCluster(content.GetIstiodInfo, cp, archive.IstiodPath(tempDir, namespace, pod), &mandatoryWg)
   352  			getIstiodLogs(runner, config, resources, namespace, pod, &mandatoryWg)
   353  
   354  		case common.IsOperatorContainer(params.ClusterVersion, container):
   355  			getOperatorLogs(runner, config, resources, namespace, pod, &optionalWg)
   356  		}
   357  	}
   358  
   359  	// Not all items are subject to timeout. Proceed only if the non-cancellable items have completed.
   360  	mandatoryWg.Wait()
   361  
   362  	// If log fetches have completed, cancel the timeout.
   363  	go func() {
   364  		optionalWg.Wait()
   365  		cmdTimer.Reset(0)
   366  	}()
   367  
   368  	// Wait for log fetches, up to the timeout.
   369  	<-cmdTimer.C
   370  
   371  	// Find the timeout duration left for the analysis process.
   372  	analyzeTimeout := time.Until(beginTime.Add(time.Duration(config.CommandTimeout)))
   373  
   374  	// Analyze runs many queries internally, so run these queries sequentially and after everything else has finished.
   375  	runAnalyze(config, params, analyzeTimeout)
   376  }
   377  
   378  // getFromCluster runs a cluster info fetching function f against the cluster and writes the results to fileName.
   379  // Runs if a goroutine, with errors reported through gErrors.
   380  func getFromCluster(f func(params *content.Params) (map[string]string, error), params *content.Params, dir string, wg *sync.WaitGroup) {
   381  	startTime := time.Now()
   382  	wg.Add(1)
   383  	log.Infof("Waiting on %s", runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
   384  	go func() {
   385  		defer func() {
   386  			wg.Done()
   387  			logRuntime(startTime, "Done getting from cluster for %v", runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
   388  		}()
   389  
   390  		out, err := f(params)
   391  		appendGlobalErr(filterUnknownBinaryErrors(err))
   392  		if err == nil {
   393  			writeFiles(dir, out, params.DryRun)
   394  		}
   395  		log.Infof("Done with %s", runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name())
   396  	}()
   397  }
   398  
   399  // filterUnknownBinaryErrors ignores errors about not finding a binary
   400  // This is expected behavior on distroless
   401  func filterUnknownBinaryErrors(err error) error {
   402  	if err == nil {
   403  		return nil
   404  	}
   405  	if strings.Contains(err.Error(), "executable file not found in $PATH") {
   406  		return nil
   407  	}
   408  	return err
   409  }
   410  
   411  // getProxyLogs fetches proxy logs for the given namespace/pod/container and stores the output in global structs.
   412  // Runs if a goroutine, with errors reported through gErrors.
   413  // TODO(stewartbutler): output the logs to a more robust/complete structure.
   414  func getProxyLogs(runner *kubectlcmd.Runner, config *config.BugReportConfig, resources *cluster2.Resources,
   415  	path, namespace, pod, container string, wg *sync.WaitGroup,
   416  ) {
   417  	startTime := time.Now()
   418  	wg.Add(1)
   419  	log.Infof("Waiting on proxy logs %v/%v/%v", namespace, pod, container)
   420  	go func() {
   421  		defer func() {
   422  			wg.Done()
   423  			logRuntime(startTime, "Done getting from proxy logs for %v/%v/%v", namespace, pod, container)
   424  		}()
   425  
   426  		clog, cstat, imp, err := getLog(runner, resources, config, namespace, pod, container)
   427  		appendGlobalErr(err)
   428  		lock.Lock()
   429  		if err == nil {
   430  			logs[path], stats[path], importance[path] = clog, cstat, imp
   431  		}
   432  		lock.Unlock()
   433  		log.Infof("Done with proxy logs %v/%v/%v", namespace, pod, container)
   434  	}()
   435  }
   436  
   437  // getIstiodLogs fetches Istiod logs for the given namespace/pod and writes the output.
   438  // Runs if a goroutine, with errors reported through gErrors.
   439  func getIstiodLogs(runner *kubectlcmd.Runner, config *config.BugReportConfig, resources *cluster2.Resources,
   440  	namespace, pod string, wg *sync.WaitGroup,
   441  ) {
   442  	startTime := time.Now()
   443  	wg.Add(1)
   444  	log.Infof("Waiting on Istiod logs for %v/%v", namespace, pod)
   445  	go func() {
   446  		defer func() {
   447  			wg.Done()
   448  			logRuntime(startTime, "Done getting Istiod logs for %v/%v", namespace, pod)
   449  		}()
   450  
   451  		clog, _, _, err := getLog(runner, resources, config, namespace, pod, common.DiscoveryContainerName)
   452  		appendGlobalErr(err)
   453  		writeFile(filepath.Join(archive.IstiodPath(tempDir, namespace, pod), "discovery.log"), clog, config.DryRun)
   454  		log.Infof("Done with Istiod logs for %v/%v", namespace, pod)
   455  	}()
   456  }
   457  
   458  // getOperatorLogs fetches istio-operator logs for the given namespace/pod and writes the output.
   459  func getOperatorLogs(runner *kubectlcmd.Runner, config *config.BugReportConfig, resources *cluster2.Resources,
   460  	namespace, pod string, wg *sync.WaitGroup,
   461  ) {
   462  	startTime := time.Now()
   463  	wg.Add(1)
   464  	log.Infof("Waiting on operator logs for %v/%v", namespace, pod)
   465  	go func() {
   466  		defer func() {
   467  			wg.Done()
   468  			logRuntime(startTime, "Done getting operator logs for %v/%v", namespace, pod)
   469  		}()
   470  
   471  		clog, _, _, err := getLog(runner, resources, config, namespace, pod, common.OperatorContainerName)
   472  		appendGlobalErr(err)
   473  		writeFile(filepath.Join(archive.OperatorPath(tempDir, namespace, pod), "operator.log"), clog, config.DryRun)
   474  		log.Infof("Done with operator logs for %v/%v", namespace, pod)
   475  	}()
   476  }
   477  
   478  // getCniLogs fetches Cni logs from istio-cni-node daemonsets inside namespace kube-system and writes the output
   479  // Runs if a goroutine, with errors reported through gErrors
   480  func getCniLogs(runner *kubectlcmd.Runner, config *config.BugReportConfig, resources *cluster2.Resources,
   481  	namespace, pod string, wg *sync.WaitGroup,
   482  ) {
   483  	startTime := time.Now()
   484  	wg.Add(1)
   485  	log.Infof("Waiting on CNI logs for %v", pod)
   486  	go func() {
   487  		defer func() {
   488  			wg.Done()
   489  			logRuntime(startTime, "Done getting CNI logs for %v", pod)
   490  		}()
   491  
   492  		clog, _, _, err := getLog(runner, resources, config, namespace, pod, "")
   493  		appendGlobalErr(err)
   494  		writeFile(filepath.Join(archive.CniPath(tempDir, pod), "cni.log"), clog, config.DryRun)
   495  		log.Infof("Done with CNI logs %v", pod)
   496  	}()
   497  }
   498  
   499  // getLog fetches the logs for the given namespace/pod/container and returns the log text and stats for it.
   500  func getLog(runner *kubectlcmd.Runner, resources *cluster2.Resources, config *config.BugReportConfig,
   501  	namespace, pod, container string,
   502  ) (string, *processlog.Stats, int, error) {
   503  	defer logRuntime(time.Now(), "Done getting logs only for %v/%v/%v", namespace, pod, container)
   504  
   505  	log.Infof("Getting logs for %s/%s/%s...", namespace, pod, container)
   506  	clog, err := runner.Logs(namespace, pod, container, false, config.DryRun)
   507  	if err != nil {
   508  		return "", nil, 0, err
   509  	}
   510  	if resources.ContainerRestarts(namespace, pod, container, common.IsCniPod(pod)) > 0 {
   511  		pclog, err := runner.Logs(namespace, pod, container, true, config.DryRun)
   512  		if err != nil {
   513  			return "", nil, 0, err
   514  		}
   515  		clog = "========= Previous log present (appended at the end) =========\n\n" + clog +
   516  			"\n\n========= Previous log =========\n\n" + pclog
   517  	}
   518  	var cstat *processlog.Stats
   519  	clog, cstat = processlog.Process(config, clog)
   520  	return clog, cstat, cstat.Importance(), nil
   521  }
   522  
   523  func runAnalyze(config *config.BugReportConfig, params *content.Params, analyzeTimeout time.Duration) {
   524  	newParam := params.SetNamespace(common.NamespaceAll)
   525  
   526  	defer logRuntime(time.Now(), "Done running Istio analyze on all namespaces and report")
   527  
   528  	common.LogAndPrintf("Running Istio analyze on all namespaces and report as below:")
   529  	out, err := content.GetAnalyze(newParam.SetIstioNamespace(config.IstioNamespace), analyzeTimeout)
   530  	if err != nil {
   531  		log.Error(err.Error())
   532  		return
   533  	}
   534  	common.LogAndPrintf("\nAnalysis Report:\n")
   535  	common.LogAndPrintf(out[common.StrNamespaceAll])
   536  	common.LogAndPrintf("\n")
   537  	writeFiles(archive.AnalyzePath(tempDir, common.StrNamespaceAll), out, config.DryRun)
   538  }
   539  
   540  func writeFiles(dir string, files map[string]string, dryRun bool) {
   541  	defer logRuntime(time.Now(), "Done writing files for dir %v", dir)
   542  	for fname, text := range files {
   543  		writeFile(filepath.Join(dir, fname), text, dryRun)
   544  	}
   545  }
   546  
   547  func writeFile(path, text string, dryRun bool) {
   548  	if dryRun {
   549  		return
   550  	}
   551  	if strings.TrimSpace(text) == "" {
   552  		return
   553  	}
   554  	mkdirOrExit(path)
   555  
   556  	defer logRuntime(time.Now(), "Done writing file for path %v", path)
   557  
   558  	if err := os.WriteFile(path, []byte(text), 0o644); err != nil {
   559  		log.Errorf(err.Error())
   560  	}
   561  }
   562  
   563  func mkdirOrExit(fpath string) {
   564  	if err := os.MkdirAll(path.Dir(fpath), 0o755); err != nil {
   565  		fmt.Printf("Could not create output directories: %s", err)
   566  		os.Exit(-1)
   567  	}
   568  }
   569  
   570  func appendGlobalErr(err error) {
   571  	if err == nil {
   572  		return
   573  	}
   574  	lock.Lock()
   575  	gErrors = util.AppendErr(gErrors, err)
   576  	lock.Unlock()
   577  }
   578  
   579  func configLogs(opt *log.Options) error {
   580  	logDir := filepath.Join(archive.OutputRootDir(tempDir), "bug-report.log")
   581  	mkdirOrExit(logDir)
   582  	f, err := os.Create(logDir)
   583  	if err != nil {
   584  		return err
   585  	}
   586  	f.Close()
   587  	op := []string{logDir}
   588  	opt2 := *opt
   589  	opt2.OutputPaths = op
   590  	opt2.ErrorOutputPaths = op
   591  	opt2.SetDefaultOutputLevel("default", log.InfoLevel)
   592  
   593  	return log.Configure(&opt2)
   594  }
   595  
   596  func logRuntime(start time.Time, format string, args ...any) {
   597  	log.WithLabels("runtime", time.Since(start)).Infof(format, args...)
   598  }