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 }