github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/list_logs.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package cluster
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"os"
    26  	"strings"
    27  
    28  	"github.com/spf13/cobra"
    29  	corev1 "k8s.io/api/core/v1"
    30  	"k8s.io/cli-runtime/pkg/genericiooptions"
    31  	"k8s.io/client-go/dynamic"
    32  	"k8s.io/client-go/kubernetes"
    33  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    34  	"k8s.io/kubectl/pkg/util/templates"
    35  
    36  	appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1"
    37  	"github.com/1aal/kubeblocks/pkg/cli/cluster"
    38  	"github.com/1aal/kubeblocks/pkg/cli/exec"
    39  	"github.com/1aal/kubeblocks/pkg/cli/printer"
    40  	"github.com/1aal/kubeblocks/pkg/cli/types"
    41  	"github.com/1aal/kubeblocks/pkg/cli/util"
    42  	"github.com/1aal/kubeblocks/pkg/constant"
    43  )
    44  
    45  var (
    46  	logsListExample = templates.Examples(`
    47  		# Display supported log files in cluster mycluster with all instance
    48  		kbcli cluster list-logs mycluster
    49  
    50  		# Display supported log files in cluster mycluster with specify component my-component
    51  		kbcli cluster list-logs mycluster --component my-component
    52  
    53  		# Display supported log files in cluster mycluster with specify instance my-instance-0
    54  		kbcli cluster list-logs mycluster --instance my-instance-0`)
    55  )
    56  
    57  // ListLogsOptions declares the arguments accepted by the list-logs command
    58  type ListLogsOptions struct {
    59  	namespace     string
    60  	clusterName   string
    61  	componentName string
    62  	instName      string
    63  
    64  	dynamicClient dynamic.Interface
    65  	clientSet     *kubernetes.Clientset
    66  	factory       cmdutil.Factory
    67  	genericiooptions.IOStreams
    68  	exec *exec.ExecOptions
    69  }
    70  
    71  func NewListLogsCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    72  	o := &ListLogsOptions{
    73  		factory:   f,
    74  		IOStreams: streams,
    75  	}
    76  
    77  	cmd := &cobra.Command{
    78  		Use:               "list-logs NAME",
    79  		Short:             "List supported log files in cluster.",
    80  		Example:           logsListExample,
    81  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
    82  		Run: func(cmd *cobra.Command, args []string) {
    83  			util.CheckErr(o.Validate(args))
    84  			util.CheckErr(o.Complete(f, args))
    85  			util.CheckErr(o.Run())
    86  		},
    87  	}
    88  	cmd.Flags().StringVarP(&o.instName, "instance", "i", "", "Instance name.")
    89  	cmd.Flags().StringVar(&o.componentName, "component", "", "Component name.")
    90  	return cmd
    91  }
    92  
    93  func (o *ListLogsOptions) Validate(args []string) error {
    94  	if len(args) < 1 {
    95  		return fmt.Errorf("must specify the cluster name")
    96  	}
    97  	return nil
    98  }
    99  
   100  func (o *ListLogsOptions) Complete(f cmdutil.Factory, args []string) error {
   101  	// set cluster name from args
   102  	o.clusterName = args[0]
   103  	config, err := o.factory.ToRESTConfig()
   104  	if err != nil {
   105  		return err
   106  	}
   107  	o.namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
   108  	if err != nil {
   109  		return err
   110  	}
   111  	o.clientSet, err = o.factory.KubernetesClientSet()
   112  	if err != nil {
   113  		return err
   114  	}
   115  	o.dynamicClient, err = f.DynamicClient()
   116  	o.exec = exec.NewExecOptions(o.factory, o.IOStreams)
   117  	o.exec.Config = config
   118  	// hide unnecessary output
   119  	o.exec.Quiet = true
   120  	return err
   121  }
   122  
   123  func (o *ListLogsOptions) Run() error {
   124  	clusterGetter := cluster.ObjectsGetter{
   125  		Client:    o.clientSet,
   126  		Dynamic:   o.dynamicClient,
   127  		Name:      o.clusterName,
   128  		Namespace: o.namespace,
   129  		GetOptions: cluster.GetOptions{
   130  			WithClusterDef: true,
   131  			WithPod:        true,
   132  		},
   133  	}
   134  	dataObj, err := clusterGetter.Get()
   135  	if err != nil {
   136  		return err
   137  	}
   138  	if err := o.printListLogs(dataObj); err != nil {
   139  		return err
   140  	}
   141  	return nil
   142  }
   143  
   144  // printListLogs prints the result of list-logs command to stdout.
   145  func (o *ListLogsOptions) printListLogs(dataObj *cluster.ClusterObjects) error {
   146  	tbl := printer.NewTablePrinter(o.Out)
   147  	logFilesData := o.gatherLogFilesData(dataObj.Cluster, dataObj.ClusterDef, dataObj.Pods)
   148  	if len(logFilesData) == 0 {
   149  		fmt.Fprintf(o.ErrOut, "No log files found. You can enable the log feature with the kbcli command below.\n"+
   150  			"kbcli cluster update %s --enable-all-logs=true --namespace %s\n", dataObj.Cluster.Name, dataObj.Cluster.Namespace)
   151  	} else {
   152  		tbl.SetHeader("INSTANCE", "LOG-TYPE", "FILE-PATH", "SIZE", "LAST-WRITTEN", "COMPONENT")
   153  		for _, f := range logFilesData {
   154  			tbl.AddRow(f.instance, f.logType, f.filePath, f.size, f.lastWritten, f.component)
   155  		}
   156  		tbl.Print()
   157  	}
   158  	return nil
   159  }
   160  
   161  type logFileInfo struct {
   162  	instance    string
   163  	logType     string
   164  	filePath    string
   165  	size        string
   166  	lastWritten string
   167  	component   string
   168  }
   169  
   170  // gatherLogFilesData gathers all log files data from each instance of the cluster.
   171  func (o *ListLogsOptions) gatherLogFilesData(c *appsv1alpha1.Cluster, cd *appsv1alpha1.ClusterDefinition, pods *corev1.PodList) []logFileInfo {
   172  	logFileInfoList := make([]logFileInfo, 0, len(pods.Items))
   173  	for _, p := range pods.Items {
   174  		if len(o.instName) > 0 && !strings.EqualFold(p.Name, o.instName) {
   175  			continue
   176  		}
   177  		componentName, ok := p.Labels[constant.KBAppComponentLabelKey]
   178  		if !ok || (len(o.componentName) > 0 && !strings.EqualFold(o.componentName, componentName)) {
   179  			continue
   180  		}
   181  		var compDefName string
   182  		logTypeMap := make(map[string]struct{})
   183  		// find component compDefName and enabledLogs config against componentName in pod's label.
   184  		for _, comCluster := range c.Spec.ComponentSpecs {
   185  			if !strings.EqualFold(comCluster.Name, componentName) {
   186  				continue
   187  			}
   188  			compDefName = comCluster.ComponentDefRef
   189  			for _, logType := range comCluster.EnabledLogs {
   190  				logTypeMap[logType] = struct{}{}
   191  			}
   192  			break
   193  		}
   194  		if len(compDefName) == 0 || len(logTypeMap) == 0 {
   195  			continue
   196  		}
   197  		for _, com := range cd.Spec.ComponentDefs {
   198  			if !strings.EqualFold(com.Name, compDefName) {
   199  				continue
   200  			}
   201  			for _, logConfig := range com.LogConfigs {
   202  				if _, ok := logTypeMap[logConfig.Name]; ok {
   203  					realFile, err := o.getRealFileFromContainer(&p, logConfig.FilePathPattern)
   204  					if err == nil {
   205  						logFileInfoList = append(logFileInfoList, convertToLogFileInfo(realFile, logConfig.Name, p.Name, componentName)...)
   206  					}
   207  				}
   208  			}
   209  			break
   210  		}
   211  	}
   212  	return logFileInfoList
   213  }
   214  
   215  // convertToLogFileInfo converts file info in string format to logFileInfo struct.
   216  func convertToLogFileInfo(fileInfo, logType, instName, component string) []logFileInfo {
   217  	fileList := strings.Split(fileInfo, "\n")
   218  	logFileList := make([]logFileInfo, 0, len(fileList))
   219  	for _, file := range fileList {
   220  		fieldList := strings.Fields(file)
   221  		if len(fieldList) == 0 {
   222  			continue
   223  		}
   224  		logFileList = append(logFileList, logFileInfo{
   225  			instance:    instName,
   226  			component:   component,
   227  			logType:     logType,
   228  			size:        fieldList[4],
   229  			lastWritten: strings.Join(fieldList[5:10], " "),
   230  			filePath:    fieldList[10],
   231  		})
   232  	}
   233  	return logFileList
   234  }
   235  
   236  // getRealFileFromContainer gets real log files against pattern from container, and returns file info in string format
   237  func (o *ListLogsOptions) getRealFileFromContainer(pod *corev1.Pod, pattern string) (string, error) {
   238  	o.exec.Pod = pod
   239  	// linux cmd : ls -lh --time-style='+%b %d, %Y %H:%M (UTC%:z)' pattern
   240  	o.exec.Command = []string{"/bin/bash", "-c", "ls -lh --time-style='+%b %d, %Y %H:%M (UTC%:z)' " + pattern}
   241  	// set customized output
   242  	out := bytes.Buffer{}
   243  	o.exec.Out = &out
   244  	o.exec.ErrOut = os.Stdout
   245  	o.exec.TTY = false
   246  	if err := o.exec.Run(); err != nil {
   247  		return out.String(), err
   248  	}
   249  	return out.String(), nil
   250  }