github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cluster/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  	"context"
    24  	"fmt"
    25  	"io"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/spf13/cobra"
    31  	corev1 "k8s.io/api/core/v1"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/cli-runtime/pkg/genericiooptions"
    34  	cmdlogs "k8s.io/kubectl/pkg/cmd/logs"
    35  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    36  	"k8s.io/kubectl/pkg/polymorphichelpers"
    37  	"k8s.io/kubectl/pkg/util/templates"
    38  
    39  	"github.com/1aal/kubeblocks/pkg/cli/cluster"
    40  	"github.com/1aal/kubeblocks/pkg/cli/exec"
    41  	"github.com/1aal/kubeblocks/pkg/cli/types"
    42  	"github.com/1aal/kubeblocks/pkg/cli/util"
    43  	"github.com/1aal/kubeblocks/pkg/constant"
    44  )
    45  
    46  var (
    47  	logsExample = templates.Examples(`
    48  		# Return snapshot logs from cluster mycluster with default primary instance (stdout)
    49  		kbcli cluster logs mycluster
    50  
    51  		# Display only the most recent 20 lines from cluster mycluster with default primary instance (stdout)
    52  		kbcli cluster logs mycluster --tail=20
    53  
    54  		# Display stdout info of specific instance my-instance-0 (cluster name comes from annotation app.kubernetes.io/instance)
    55  		kbcli cluster logs --instance my-instance-0
    56  
    57  		# Return snapshot logs from cluster mycluster with specific instance my-instance-0 (stdout)
    58  		kbcli cluster logs mycluster --instance my-instance-0
    59  
    60  		# Return snapshot logs from cluster mycluster with specific instance my-instance-0 and specific container
    61          # my-container (stdout)
    62  		kbcli cluster logs mycluster --instance my-instance-0 -c my-container
    63  
    64  		# Return slow logs from cluster mycluster with default primary instance
    65  		kbcli cluster logs mycluster --file-type=slow
    66  
    67  		# Begin streaming the slow logs from cluster mycluster with default primary instance
    68  		kbcli cluster logs -f mycluster --file-type=slow
    69  
    70  		# Return the specific file logs from cluster mycluster with specific instance my-instance-0
    71  		kbcli cluster logs mycluster --instance my-instance-0 --file-path=/var/log/yum.log
    72  
    73  		# Return the specific file logs from cluster mycluster with specific instance my-instance-0 and specific
    74          # container my-container
    75  		kbcli cluster logs mycluster --instance my-instance-0 -c my-container --file-path=/var/log/yum.log`)
    76  )
    77  
    78  // LogsOptions declares the arguments accepted by the logs command
    79  type LogsOptions struct {
    80  	clusterName string
    81  	fileType    string
    82  	filePath    string
    83  	*exec.ExecOptions
    84  	logOptions cmdlogs.LogsOptions
    85  }
    86  
    87  // NewLogsCmd returns the logic of accessing cluster log file
    88  func NewLogsCmd(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    89  	l := &LogsOptions{
    90  		ExecOptions: exec.NewExecOptions(f, streams),
    91  		logOptions: cmdlogs.LogsOptions{
    92  			IOStreams: streams,
    93  		},
    94  	}
    95  	cmd := &cobra.Command{
    96  		Use:               "logs NAME",
    97  		Short:             "Access cluster log file.",
    98  		Example:           logsExample,
    99  		ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()),
   100  		Run: func(cmd *cobra.Command, args []string) {
   101  			util.CheckErr(l.ExecOptions.Complete())
   102  			util.CheckErr(l.complete(args))
   103  			util.CheckErr(l.validate())
   104  			util.CheckErr(l.run())
   105  		},
   106  	}
   107  	l.addFlags(cmd)
   108  	return cmd
   109  }
   110  
   111  func (o *LogsOptions) addFlags(cmd *cobra.Command) {
   112  	cmd.Flags().StringVarP(&o.PodName, "instance", "i", "", "Instance name.")
   113  	cmd.Flags().StringVarP(&o.logOptions.Container, "container", "c", "", "Container name.")
   114  	cmd.Flags().BoolVarP(&o.logOptions.Follow, "follow", "f", false, "Specify if the logs should be streamed.")
   115  	cmd.Flags().Int64Var(&o.logOptions.Tail, "tail", -1, "Lines of recent log file to display. Defaults to -1 for showing all log lines.")
   116  	cmd.Flags().Int64Var(&o.logOptions.LimitBytes, "limit-bytes", 0, "Maximum bytes of logs to return.")
   117  	cmd.Flags().BoolVar(&o.logOptions.Prefix, "prefix", false, "Prefix each log line with the log source (pod name and container name). Only take effect for stdout&stderr.")
   118  	cmd.Flags().BoolVar(&o.logOptions.IgnoreLogErrors, "ignore-errors", false, "If watching / following pod logs, allow for any errors that occur to be non-fatal. Only take effect for stdout&stderr.")
   119  	cmd.Flags().BoolVar(&o.logOptions.Timestamps, "timestamps", false, "Include timestamps on each line in the log output. Only take effect for stdout&stderr.")
   120  	cmd.Flags().StringVar(&o.logOptions.SinceTime, "since-time", o.logOptions.SinceTime, "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used. Only take effect for stdout&stderr.")
   121  	cmd.Flags().DurationVar(&o.logOptions.SinceSeconds, "since", o.logOptions.SinceSeconds, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used. Only take effect for stdout&stderr.")
   122  	cmd.Flags().BoolVarP(&o.logOptions.Previous, "previous", "p", o.logOptions.Previous, "If true, print the logs for the previous instance of the container in a pod if it exists. Only take effect for stdout&stderr.")
   123  
   124  	cmd.Flags().StringVar(&o.fileType, "file-type", "", "Log-file type. List them with list-logs cmd. When file-path and file-type are unset, output stdout/stderr of target container.")
   125  	cmd.Flags().StringVar(&o.filePath, "file-path", "", "Log-file path. File path has a priority over file-type. When file-path and file-type are unset, output stdout/stderr of target container.")
   126  
   127  	cmd.MarkFlagsMutuallyExclusive("file-path", "file-type")
   128  	cmd.MarkFlagsMutuallyExclusive("since", "since-time")
   129  }
   130  
   131  // run customs logic for logs
   132  func (o *LogsOptions) run() error {
   133  	if o.isStdoutForContainer() {
   134  		return o.runLogs()
   135  	}
   136  	return o.ExecOptions.Run()
   137  }
   138  
   139  // complete customs complete function for logs
   140  func (o *LogsOptions) complete(args []string) error {
   141  	if len(args) == 0 && len(o.PodName) == 0 {
   142  		return fmt.Errorf("cluster name or instance name should be specified")
   143  	}
   144  	if len(args) > 0 {
   145  		o.clusterName = args[0]
   146  	}
   147  	// podName not set, find the default pod of cluster
   148  	if len(o.PodName) == 0 {
   149  		infos := cluster.GetSimpleInstanceInfos(o.Dynamic, o.clusterName, o.Namespace)
   150  		if len(infos) == 0 || infos[0].Name == constant.ComponentStatusDefaultPodName {
   151  			return fmt.Errorf("failed to find the default instance, please check cluster status")
   152  		}
   153  		// first element is the default instance to connect
   154  		o.PodName = infos[0].Name
   155  	}
   156  	pod, err := o.Client.CoreV1().Pods(o.Namespace).Get(context.TODO(), o.PodName, metav1.GetOptions{})
   157  	if err != nil {
   158  		return err
   159  	}
   160  	// cluster name is not specified, get from pod label
   161  	if o.clusterName == "" {
   162  		if name, ok := pod.Annotations[constant.AppInstanceLabelKey]; !ok {
   163  			return fmt.Errorf("failed to find the cluster to which the instance belongs")
   164  		} else {
   165  			o.clusterName = name
   166  		}
   167  	}
   168  	var command string
   169  	switch {
   170  	case len(o.filePath) > 0:
   171  		command = assembleTail(o.logOptions.Follow, o.logOptions.Tail, o.logOptions.LimitBytes) + " " + o.filePath
   172  	case o.isStdoutForContainer():
   173  		{
   174  			// file-path and file-type are unset, output container's stdout & stderr, like kubectl logs
   175  			o.logOptions.RESTClientGetter = o.Factory
   176  			o.logOptions.LogsForObject = polymorphichelpers.LogsForObjectFn
   177  			o.logOptions.Object = pod
   178  			o.logOptions.Options, _ = o.logOptions.ToLogOptions()
   179  		}
   180  	default: // find corresponding file path by file type
   181  		{
   182  			clusterGetter := cluster.ObjectsGetter{
   183  				Client:    o.Client,
   184  				Dynamic:   o.Dynamic,
   185  				Name:      o.clusterName,
   186  				Namespace: o.Namespace,
   187  				GetOptions: cluster.GetOptions{
   188  					WithClusterDef: true,
   189  				},
   190  			}
   191  			obj, err := clusterGetter.Get()
   192  			if err != nil {
   193  				return err
   194  			}
   195  			if command, err = o.createFileTypeCommand(pod, obj); err != nil {
   196  				return err
   197  			}
   198  		}
   199  	}
   200  	o.Command = []string{"/bin/bash", "-c", command}
   201  	o.ContainerName = o.logOptions.Container
   202  	o.Pod = pod
   203  	// hide unnecessary output
   204  	o.Quiet = true
   205  	return nil
   206  }
   207  
   208  func (o *LogsOptions) validate() error {
   209  	if len(o.clusterName) == 0 {
   210  		return fmt.Errorf("cluster name must be specified")
   211  	}
   212  	if o.logOptions.LimitBytes < 0 {
   213  		return fmt.Errorf("--limit-bytes must be greater than 0")
   214  	}
   215  	if o.logOptions.Tail < -1 {
   216  		return fmt.Errorf("--tail must be greater than or equal to -1")
   217  	}
   218  	if o.isStdoutForContainer() {
   219  		if len(o.logOptions.SinceTime) > 0 && o.logOptions.SinceSeconds != 0 {
   220  			return fmt.Errorf("at most one of `sinceTime` or `sinceSeconds` may be specified")
   221  		}
   222  
   223  		logsOptions, ok := o.logOptions.Options.(*corev1.PodLogOptions)
   224  		if !ok {
   225  			return fmt.Errorf("unexpected logs options object")
   226  		}
   227  		if logsOptions.SinceSeconds != nil && *logsOptions.SinceSeconds < int64(0) {
   228  			return fmt.Errorf("--since must be greater than 0")
   229  		}
   230  
   231  		if logsOptions.TailLines != nil && *logsOptions.TailLines < -1 {
   232  			return fmt.Errorf("--tail must be greater than or equal to -1")
   233  		}
   234  	}
   235  	return nil
   236  }
   237  
   238  // createFileTypeCommand creates command against log file type
   239  func (o *LogsOptions) createFileTypeCommand(pod *corev1.Pod, obj *cluster.ClusterObjects) (string, error) {
   240  	var command string
   241  	componentName, ok := pod.Labels[constant.KBAppComponentLabelKey]
   242  	if !ok {
   243  		return command, fmt.Errorf("get component name from pod labels fail")
   244  	}
   245  	var compDefName string
   246  	for _, comCluster := range obj.Cluster.Spec.ComponentSpecs {
   247  		if strings.EqualFold(comCluster.Name, componentName) {
   248  			compDefName = comCluster.ComponentDefRef
   249  			break
   250  		}
   251  	}
   252  	if len(compDefName) == 0 {
   253  		return command, fmt.Errorf("get pod component definition name in cluster.yaml fail")
   254  	}
   255  	var filePathPattern string
   256  	for _, com := range obj.ClusterDef.Spec.ComponentDefs {
   257  		if strings.EqualFold(com.Name, compDefName) {
   258  			for _, logConfig := range com.LogConfigs {
   259  				if strings.EqualFold(logConfig.Name, o.fileType) {
   260  					filePathPattern = logConfig.FilePathPattern
   261  					break
   262  				}
   263  			}
   264  			break
   265  		}
   266  	}
   267  	if len(filePathPattern) > 0 {
   268  		command = "ls " + filePathPattern + " | xargs " + assembleTail(o.logOptions.Follow, o.logOptions.Tail, o.logOptions.LimitBytes)
   269  	} else {
   270  		return command, fmt.Errorf("can't get file path pattern by type %s", o.fileType)
   271  	}
   272  	return command, nil
   273  }
   274  
   275  // assembleCommand assembles tail command for log file
   276  func assembleTail(follow bool, tail int64, limitBytes int64) string {
   277  	command := make([]string, 0, 5)
   278  	command = append(command, "tail")
   279  	if follow {
   280  		command = append(command, "-f")
   281  	}
   282  	if tail == -1 {
   283  		command = append(command, "--lines=+1")
   284  	} else {
   285  		command = append(command, "--lines="+strconv.FormatInt(tail, 10))
   286  	}
   287  	if limitBytes > 0 {
   288  		command = append(command, "--bytes="+strconv.FormatInt(limitBytes, 10))
   289  	}
   290  	return strings.Join(command, " ")
   291  }
   292  
   293  func (o *LogsOptions) isStdoutForContainer() bool {
   294  	if len(o.filePath) == 0 {
   295  		return len(o.fileType) == 0 || strings.EqualFold(o.fileType, "stdout") || strings.EqualFold(o.fileType, "stderr")
   296  	}
   297  	return false
   298  }
   299  
   300  // runLogs retrieves stdout/stderr logs
   301  func (o *LogsOptions) runLogs() error {
   302  	requests, err := o.logOptions.LogsForObject(o.logOptions.RESTClientGetter, o.logOptions.Object, o.logOptions.Options, 60*time.Second, false)
   303  	if err != nil {
   304  		return err
   305  	}
   306  	for objRef, request := range requests {
   307  		out := o.addPrefixIfNeeded(objRef, o.Out)
   308  		if err := cmdlogs.DefaultConsumeRequest(request, out); err != nil {
   309  			if !o.logOptions.IgnoreLogErrors {
   310  				return err
   311  			}
   312  			fmt.Fprintf(o.Out, "error: %v\n", err)
   313  		}
   314  	}
   315  	return nil
   316  }
   317  
   318  func (o *LogsOptions) addPrefixIfNeeded(ref corev1.ObjectReference, writer io.Writer) io.Writer {
   319  	if !o.logOptions.Prefix || ref.FieldPath == "" || ref.Name == "" {
   320  		return writer
   321  	}
   322  	prefix := fmt.Sprintf("[pod/%s/%s] ", ref.Name, o.ContainerName)
   323  	return &prefixingWriter{
   324  		prefix: []byte(prefix),
   325  		writer: writer,
   326  	}
   327  }
   328  
   329  type prefixingWriter struct {
   330  	prefix []byte
   331  	writer io.Writer
   332  }
   333  
   334  func (pw *prefixingWriter) Write(p []byte) (int, error) {
   335  	if len(p) == 0 {
   336  		return 0, nil
   337  	}
   338  	n, err := pw.writer.Write(append(pw.prefix, p...))
   339  	if n > len(p) {
   340  		// To comply with the io.Writer interface requirements we must
   341  		// return a number of bytes written from p (0 <= n <= len(p)),
   342  		// so we are ignoring the length of the prefix here.
   343  		return len(p), err
   344  	}
   345  	return n, err
   346  }