diff --git a/doc/diagnostic-tools.md b/doc/diagnostic-tools.md new file mode 100644 index 000000000..24b77e3c4 --- /dev/null +++ b/doc/diagnostic-tools.md @@ -0,0 +1,68 @@ +[NooBaa Operator](../README.md) / +# Diagnostic Tools +This document explains and elaborates on the diagnostic tools provided by the NooBaa CLI when the `diagnostics` parameter is used. These tools are designed to help the user get a comprehensive overview of their system's status, as well as troubleshooting problems. + +## Log Analysis +The log analyzer (`log-analysis`) lets the user search for strings in the logs of all NooBaa pods and containers, including `--previous` ones. It provides a clear and easy-to-use report with the number of times the searched string was found, alongside the first and last occurrence timestamps to provide an idea of the time window that the searched string applies to. +The first (and only) positional parameter expects to receive the search string; if no positional parameter is provided, the tool will prompt the user to enter the search string. + +The analyzer supports several options to allow for more advanced queries: +- `-i, --case-insensitive` - Allows the user to search for a string in a case-insensitive manner (similar to `grep -i`) +- `--fuzzy` - Activates fuzzy-matching, finding matches for any cases where the search string is a subset of a log line (even if not sequentially; e.g. when used with the -i parameter, “s3error” would find “s3 error”, “S3Error”, and “s3-error”) +- `--noobaa-time` - By default, the tool uses Kubernetes timestamps, since in some cases, certain NooBaa log lines may not have timestamps. The NooBaa timestamps are at an offset of 5~20ms from the Kubernetes ones, thus it is recommended to use one type of timestamp consistently. +- `--prefer-line` - When used in conjunction with `--noobaa-time`, will print a log line instead of an error when no timestamp is present in the line +- `--tail` - Tells the tool to fetch only the last N lines of the logs (similar to `minikube logs --tail N`) +- `-v, --verbose` - Prints every matching log line +- `-w, --whole-string` - Activates whole-string matching, yielding results only if the search string appears in a log line while surrounded by non-word characters; e.g. "Reconcile" will match with "BucketClass Reconcile..." but not "ReconcileObject" (similar to `grep -w`) + +### Example: +In this example, the tool is used to find case-insensitive, fuzzy occurrences of "s3error" in the last 1,000 lines - +```bash +$ nb diagnostics log-analysis --tail 1000 -i --fuzzy s3error +INFO[0000] +INFO[0000] ✨────────────────────────────────────────────✨ +INFO[0000] Collecting and analyzing pod logs - +INFO[0000] Search string: s3error +INFO[0000] Case insensitivity: true +INFO[0000] Match whole string: false +INFO[0000] From the last 1000 lines +INFO[0000] Using Kubernetes timestamps +INFO[0000] Found occurrences will be printed below +INFO[0000] in the format : +INFO[0000] ✨────────────────────────────────────────────✨ +INFO[0000] Analyzing noobaa-core-0:core +INFO[0001] Hits: 411 +INFO[0001] Earliest appearance: 2024-12-11T09:04:05.245904744Z +INFO[0001] Latest appearance: 2024-12-11T09:11:51.054808004Z +INFO[0001] ────────────────────────────────────────────────────────────────────────────────── +INFO[0001] Analyzing noobaa-core-0:noobaa-log-processor +INFO[0001] No occurrences found +INFO[0001] ────────────────────────────────────────────────────────────────────────────────── +INFO[0001] Analyzing noobaa-db-pg-0:db +INFO[0001] No occurrences found +INFO[0001] ────────────────────────────────────────────────────────────────────────────────── +INFO[0001] Analyzing noobaa-default-backing-store-noobaa-pod-08270bcd:noobaa-agent +INFO[0001] Hits: 111 +INFO[0001] Earliest appearance: 2024-12-11T08:36:42.050234365Z +INFO[0001] Latest appearance: 2024-12-11T09:11:20.498970755Z +INFO[0001] ────────────────────────────────────────────────────────────────────────────────── +INFO[0001] Analyzing noobaa-endpoint-7995576ddc-tq9p4:endpoint +INFO[0001] Hits: 6 +INFO[0001] Earliest appearance: 2024-12-11T08:32:04.879477461Z +INFO[0001] Latest appearance: 2024-12-11T08:36:27.793515138Z +INFO[0001] ────────────────────────────────────────────────────────────────────────────────── +INFO[0001] Analyzing noobaa-operator-68f66b987b-dz4xw:noobaa-operator +INFO[0001] Hits: 10 +INFO[0001] Earliest appearance: 2024-12-11T09:10:43.972246776Z +INFO[0001] Latest appearance: 2024-12-11T09:11:50.359484454Z +INFO[0001] ────────────────────────────────────────────────────────────────────────────────── +``` + +## Resource Analysis +TODO + +## Diagnostic Information Collection +TODO + +## System Reports +TODO \ No newline at end of file diff --git a/go.mod b/go.mod index 2c3a64d6c..912aaed0c 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/kedacore/keda/v2 v2.7.0 github.com/kube-object-storage/lib-bucket-provisioner v0.0.0-20221122204822-d1a8c34382f1 github.com/libopenstorage/secrets v0.0.0-20240416031220-a17cf7f72c6c + github.com/lithammer/fuzzysearch v1.1.8 github.com/marstr/randname v0.0.0-20200428202425-99aca53a2176 github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.35.1 diff --git a/go.sum b/go.sum index 013e73201..9b7416937 100644 --- a/go.sum +++ b/go.sum @@ -718,6 +718,8 @@ github.com/libopenstorage/operator v0.0.0-20200725001727-48d03e197117/go.mod h1: github.com/libopenstorage/stork v1.3.0-beta1.0.20200630005842-9255e7a98775/go.mod h1:qBSzYTJVHlOMg5RINNiHD1kBzlasnrc2uKLPZLgu1Qs= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= diff --git a/pkg/diagnostics/collect.go b/pkg/diagnostics/collect.go index 1b2207ab4..80fd5580f 100644 --- a/pkg/diagnostics/collect.go +++ b/pkg/diagnostics/collect.go @@ -140,7 +140,7 @@ func (c *Collector) CollectPodsLogs(listOptions client.ListOptions) { c.CollectDescribe("pod", pod.Name) - podLogs, _ := util.GetPodLogs(*pod) + podLogs, _ := util.GetPodLogs(*pod, nil, false) for containerName, containerLog := range podLogs { targetFile := fmt.Sprintf("%s/%s-%s.log", c.folderName, pod.Name, containerName) err := util.SaveStreamToFile(containerLog, targetFile) diff --git a/pkg/diagnostics/diagnostics.go b/pkg/diagnostics/diagnostics.go index 215d4deef..0f907e998 100644 --- a/pkg/diagnostics/diagnostics.go +++ b/pkg/diagnostics/diagnostics.go @@ -25,6 +25,7 @@ func Cmd() *cobra.Command { CmdDbDump(), CmdAnalyze(), CmdReport(), + CmdLogAnalysis(), ) return cmd } @@ -123,6 +124,25 @@ func CmdAnalyzeResources() *cobra.Command { return cmd } +// CmdLogAnalysis returns a CLI command +func CmdLogAnalysis() *cobra.Command { + cmd := &cobra.Command{ + Use: "log-analysis", + Short: "Run the log analyzer on all NooBaa pods", + Run: RunLogAnalysis, + } + cmd.Flags().BoolP("verbose", "v", false, "Print every matching log line") + cmd.Flags().Bool("fuzzy", false, "(Experimental) Toggle fuzzy matching for the search string") + cmd.Flags().Int64("tail", 1000, "Number of lines to tail from the logs, minimum 1, default 1000") + cmd.Flags().BoolP("case-insensitive", "i", false, "Toggle search-string case insensitivity (similar to grep's -i flag)") + cmd.Flags().BoolP("whole-string", "w", false, "Match the whole search string as a single word (similar to grep's -w flag)") + cmd.Flags().Bool("prefer-line", false, "Prefer to print the line containing the search string when it doesn't contain a timestamp") + cmd.Flags().Bool("noobaa-time", false, "Use NooBaa-provided timestamps instead of the Kubernetes ones (~10ms earlier than Kubernetes)") + + + return cmd +} + /////// Deprecated Functions /////// // CmdDbDumpDeprecated returns a CLI command diff --git a/pkg/diagnostics/log_analysis.go b/pkg/diagnostics/log_analysis.go new file mode 100644 index 000000000..ad3ee0103 --- /dev/null +++ b/pkg/diagnostics/log_analysis.go @@ -0,0 +1,334 @@ +package diagnostics + +import ( + "bufio" + "fmt" + "regexp" + "strings" + "time" + + "github.com/lithammer/fuzzysearch/fuzzy" + "github.com/noobaa/noobaa-operator/v5/pkg/options" + "github.com/noobaa/noobaa-operator/v5/pkg/util" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/spf13/cobra" +) + +// LogAnalysisOptions holds parameters for log analysis +type LogAnalysisOptions struct { + verbose bool + tailLines int64 + fuzzyMatch bool + searchString string + noobaaTimestamp bool + matchWholeString bool + caseInsensitivity bool + showLineIfNoTimestamp bool +} + +const ( + noobaaOutputTimestampFormat = "02/01/2006 15:04:05" + // To be used in case of additional NooBaa operators in the future + noobaaOperatorPodNamePrefix = "noobaa-operator" + operatorTimestampPrefix = `time="` + operatorWTimestampPrefix = "W" +) + +var ( + ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`) +) + +// RunLogAnalysis runs a CLI command +func RunLogAnalysis(cmd *cobra.Command, args []string) { + verbose, _ := cmd.Flags().GetBool("verbose") + tailLines, _ := cmd.Flags().GetInt64("tail") + fuzzyMatch, _ := cmd.Flags().GetBool("fuzzy") + noobaaTimestamp, _ := cmd.Flags().GetBool("noobaa-time") + matchWholeString, _ := cmd.Flags().GetBool("whole-string") + showLineIfNoTimestamp, _ := cmd.Flags().GetBool("prefer-line") + caseInsensitivity, _ := cmd.Flags().GetBool("case-insensitive") + searchString := "" + + analysisOptions := LogAnalysisOptions{ + verbose: verbose, + tailLines: tailLines, + fuzzyMatch: fuzzyMatch, + searchString: searchString, + noobaaTimestamp: noobaaTimestamp, + matchWholeString: matchWholeString, + caseInsensitivity: caseInsensitivity, + showLineIfNoTimestamp: showLineIfNoTimestamp, + } + + validateLogAnalysisParameters(&analysisOptions, args) + podSelector, _ := labels.Parse("app=noobaa") + listOptions := client.ListOptions{Namespace: options.Namespace, LabelSelector: podSelector} + CollectAndAnalyzeLogs(listOptions, &analysisOptions) +} + +// validateLogAnalysisParameters validates the parameters for log analysis +func validateLogAnalysisParameters(analysisOptions *LogAnalysisOptions, args []string) { + log := util.Logger() + if len(args) != 1 || args[0] == "" { + analysisOptions.searchString = util.ShowStringPrompt("Provide a search string") + } else { + analysisOptions.searchString = args[0] + } + + if analysisOptions.tailLines < 1 { + log.Fatalf("❌ Tail must be a whole positive number") + } + + if analysisOptions.fuzzyMatch && analysisOptions.matchWholeString { + log.Fatalf("❌ Cannot use both fuzzy matching and whole-string matching") + } +} + +// CollectAndAnalyzeLogs collects and analyzes logs of all existing noobaa pods +func CollectAndAnalyzeLogs(listOptions client.ListOptions, analysisOptions *LogAnalysisOptions) { + log := util.Logger() + chosenTimestamp := "" + if analysisOptions.noobaaTimestamp { + chosenTimestamp = "NooBaa" + } else { + chosenTimestamp = "Kubernetes" + } + log.Println() + log.Println("✨────────────────────────────────────────────✨") + log.Printf(" Collecting and analyzing pod logs -") + log.Printf(" Search string: %s", analysisOptions.searchString) + log.Printf(" Case insensitivity: %t", analysisOptions.caseInsensitivity) + log.Printf(" Match whole string: %t", analysisOptions.matchWholeString) + log.Printf(" From the last %d lines", analysisOptions.tailLines) + log.Printf(" Using %s timestamps", chosenTimestamp) + log.Println(" Found occurrences will be printed below") + log.Println(" in the format :") + log.Println("✨────────────────────────────────────────────✨") + podList := &corev1.PodList{} + if !util.KubeList(podList, &listOptions) { + log.Printf(`❌ failed to get NooBaa pod list within namespace %s\n`, options.Namespace) + return + } + for i := range podList.Items { + pod := &podList.Items[i] + analyzePodLogs(pod, analysisOptions) + } +} + +// analyzePodLogs will count the number of occurrences of the search string in a pod log +// as well as find and print the timestamps of the first and last occurrence of +// the search string in the logs +func analyzePodLogs(pod *corev1.Pod, analysisOptions *LogAnalysisOptions) { + log := util.Logger() + podLogs, err := util.GetPodLogs(*pod, &analysisOptions.tailLines, true) + if err != nil { + log.Printf("❌ Failed to get logs for pod %s: %v", pod.Name, err) + return + } + if analysisOptions.caseInsensitivity { + analysisOptions.searchString = strings.ToLower(analysisOptions.searchString) + } + stringBoundaryRegex := compileStringBoundaryRegex(analysisOptions) // Compiled here for better efficiency + for containerName, containerLog := range podLogs { + firstAppearanceFound := false + firstAppearanceTimestamp := "" + lastAppearanceTimestamp := "" + lastOccurrenceLine := "" + log.Printf("Analyzing %s:%s", pod.Name, containerName) + defer containerLog.Close() + occurrenceCounter := 0 + scanner := bufio.NewScanner(containerLog) + for scanner.Scan() { + line := scanner.Text() + // Clean line from ANSI escape codes + if !strings.Contains(pod.Name, noobaaOperatorPodNamePrefix) { + line = sanitizeANSI(line) + } + lineContainsMatch := stringMatchCheck(line, stringBoundaryRegex, analysisOptions) + if lineContainsMatch { + if !firstAppearanceFound { + firstAppearanceFound = true + firstAppearanceTimestamp = extractTimeString(pod, line, *analysisOptions) + } + if analysisOptions.verbose { + log.Println(line) + } + occurrenceCounter++ + lastOccurrenceLine = line + } + } + lastAppearanceTimestamp = extractTimeString(pod, lastOccurrenceLine, *analysisOptions) + if occurrenceCounter == 0 { + log.Println("No occurrences found") + } else { + log.Printf("Hits: %d", occurrenceCounter) + log.Printf("Earliest appearance: %s", firstAppearanceTimestamp) + if occurrenceCounter > 1 { + log.Printf("Latest appearance: %s", lastAppearanceTimestamp) + } + } + log.Println("──────────────────────────────────────────────────────────────────────────────────") + } +} + +// sanitizeANSI removes ANSI escape codes from a string +func sanitizeANSI(line string) string { + // This is done in order to avoid the terminal from interpreting + // them as color codes and printing them as garbage characters. + return ansiRegex.ReplaceAllString(line, "") +} + +// compileStringBoundaryRegex compiles a word boundary regex pattern for the search string +func compileStringBoundaryRegex(analysisOptions *LogAnalysisOptions) *regexp.Regexp { + var stringBoundarySearchPattern *regexp.Regexp + stringBoundaryPattern := fmt.Sprintf(`\b%s\b`, regexp.QuoteMeta(analysisOptions.searchString)) + if analysisOptions.caseInsensitivity { + stringBoundarySearchPattern = regexp.MustCompile("(?i)" + stringBoundaryPattern) + } else { + stringBoundarySearchPattern = regexp.MustCompile(stringBoundaryPattern) + } + return stringBoundarySearchPattern +} + +// stringMatchCheck checks if a line contains a match to the search string +func stringMatchCheck(line string, stringBoundaryRegex *regexp.Regexp, analysisOptions *LogAnalysisOptions) bool { + if analysisOptions.matchWholeString { + return wholestringMatchCheck(line, stringBoundaryRegex) + } else { + return partialMatchCheck(line, analysisOptions) + } +} + +// wholestringMatchCheck checks if a line contains a whole string match to the search string +// Mostly used for readability and organization purposes +func wholestringMatchCheck(line string, stringBoundaryRegex *regexp.Regexp) bool { + return stringBoundaryRegex.MatchString(line) +} + +// partialMatchCheck checks if a line contains a partial/fuzzy match to the search string +func partialMatchCheck(line string, analysisOptions *LogAnalysisOptions) bool { + if analysisOptions.fuzzyMatch { + fuzzyCaseInsensitiveMatch := analysisOptions.caseInsensitivity && fuzzy.MatchNormalized(analysisOptions.searchString, line) + fuzzyCaseSensitiveMatch := fuzzy.Match(analysisOptions.searchString, line) + return fuzzyCaseInsensitiveMatch || fuzzyCaseSensitiveMatch + } else { + // Check for a match by temporarily casting the line string to lowercase + // (the search string is cast in the beginning of analyzePodLogs) + caseInsensitiveMatch := analysisOptions.caseInsensitivity && strings.Contains(strings.ToLower(line), analysisOptions.searchString) + caseSensitiveMatch := strings.Contains(line, analysisOptions.searchString) + return caseInsensitiveMatch || caseSensitiveMatch + } +} + +// extractTimeString extracts the timestamp from a log line by checking which pod +// it originated from and redirecting it to the appropriate extraction function +func extractTimeString(pod *corev1.Pod, line string, analysisOptions LogAnalysisOptions) string { + if analysisOptions.noobaaTimestamp { + if strings.Contains(pod.Name, noobaaOperatorPodNamePrefix) { + return extractOperatorTimestampString(line, analysisOptions.showLineIfNoTimestamp) + } else { + return extractCoreTimestampString(line, analysisOptions.showLineIfNoTimestamp) + } + } else { + return extractKubernetesTimestampString(line) + } +} + +// extractKubernetesTimestampString extracts the timestamp from a Kubernetes log line +func extractKubernetesTimestampString(line string) string { + // Example log line: + // 2024-12-10T07:27:16.856641898Z Dec-10 7:27:16.847 [BGWorkers/36]... + splitLine := strings.SplitN(line, " ", 2) + return splitLine[0] +} + +// extractCoreTimestampString extracts, parses and formats a timestamp string +// from pods running NooBaa Core code (core, endpoint, PV pod) +func extractCoreTimestampString(line string, showLineIfNoTimestamp bool) string { + // Example log line: + // Dec-9 15:16:31.621 [BGWorkers/36] [L0] ... + const minimumRequiredIndices = 2 + // Example split result: + // ["Dec-9", "15:16:31.621", "[BGWorkers..."] + splitLine := strings.SplitN(line, " ", 3) + if len(splitLine) < minimumRequiredIndices { + return timeParsingError(showLineIfNoTimestamp, line) + } + lineDate := splitLine[0] + lineTime := splitLine[1] + // The year is assumed to be the current one since it's not provided + year := time.Now().Year() + layout := "Jan-2-2006 15:04:05.000" + timestampString := fmt.Sprintf("%s-%d %s", lineDate, year, lineTime) + parsedTime, err := time.Parse(layout, timestampString) + if err != nil { + return timeParsingError(showLineIfNoTimestamp, line) + } + return parsedTime.Format(noobaaOutputTimestampFormat) +} + +// extractOperatorTimestampString extracts, parses, formats and returns a timestamp +// string from the NooBaa Operator pod logs +func extractOperatorTimestampString(line string, showLineIfNoTimestamp bool) string { + if strings.HasPrefix(line, operatorTimestampPrefix) { + return extractStandardOperatorTimestampString(line, showLineIfNoTimestamp) + } else if strings.HasPrefix(line, operatorWTimestampPrefix) { + return extractOperatorWTimestampString(line, showLineIfNoTimestamp) + } + return timeParsingError(showLineIfNoTimestamp, line) +} + +// extractStandardOperatorTimestampString extracts the timestamp in case of a standard operator log line +// Example: +// time="2024-12-10T07:27:36Z" level=info msg="... +func extractStandardOperatorTimestampString(line string, showLineIfNoTimestamp bool) string { + secondQuotesIndex := strings.Index(line[len(operatorTimestampPrefix):], `"`) + if secondQuotesIndex == -1 { + return timeParsingError(showLineIfNoTimestamp, line) + } + timestampString := line[len(operatorTimestampPrefix) : len(operatorTimestampPrefix)+secondQuotesIndex] + // Parse the date using RFC3339 layout + const operatorTimestampFormat = time.RFC3339 + parsedTimestamp, err := time.Parse(operatorTimestampFormat, timestampString) + if err != nil { + return timeParsingError(showLineIfNoTimestamp, line) + } + return parsedTimestamp.Format(noobaaOutputTimestampFormat) +} + +// extractOperatorWTimestampString extracts the timestamp in case of a non-standard operator log line +// Example: +// W1209 13:41:05.890285 1 reflector.go:484... +func extractOperatorWTimestampString(line string, showLineIfNoTimestamp bool) string { + const minimumWTimeParseLength = 22 + if len(line) < minimumWTimeParseLength { + return timeParsingError(showLineIfNoTimestamp, line) + } + wStringDayMonthStartIndex := 1 + wStringDayMonthEndIndex := 5 + wStringTimeStartIndex := 6 + wStringTimeEndIndex := 21 + datePart := line[wStringDayMonthStartIndex:wStringDayMonthEndIndex] + timePart := line[wStringTimeStartIndex:wStringTimeEndIndex] + day := datePart[2:] + month := datePart[:2] + // The year is assumed to be the current one since it's not provided + year := time.Now().Year() + fullTimeStr := fmt.Sprintf("%s/%s/%d %s", day, month, year, timePart) + return fullTimeStr +} + +// timeParsingError returns an error message if the timestamp +// could not be parsed based on the user's prefer-line setting +func timeParsingError(showLineIfNoTimestamp bool, line string) string { + absentTimestampErrorWithLine := fmt.Sprintf("Could not parse timestamp in line %s", line) + const absentTimestampError = "No timestamp found" + if showLineIfNoTimestamp { + return absentTimestampErrorWithLine + } + return absentTimestampError +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 5a568e1cb..8dfcec695 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -695,7 +695,9 @@ func GetPodStatusLine(pod *corev1.Pod) string { } // GetPodLogs info -func GetPodLogs(pod corev1.Pod) (map[string]io.ReadCloser, error) { +// Note: tailLines and timestamps can be passed to override the defaults in +// PodLogOptions, pass nil and false if you'd like to preserve the defaults +func GetPodLogs(pod corev1.Pod, tailLines *int64, timestamps bool) (map[string]io.ReadCloser, error) { allContainers := append(pod.Spec.InitContainers, pod.Spec.Containers...) config := KubeConfig() clientset, err := kubernetes.NewForConfig(config) @@ -726,11 +728,18 @@ func GetPodLogs(pod corev1.Pod) (map[string]io.ReadCloser, error) { // retrieve logs from a current instantiation of pod containers podLogOpts := corev1.PodLogOptions{Container: container.Name} + podLogOpts.Timestamps = timestamps + if tailLines != nil { + podLogOpts.TailLines = tailLines + } getPodLogOpts(&pod, &podLogOpts, nil) // retrieve logs from a previous instantiation of pod containers - prevPodLogOpts := corev1.PodLogOptions{Container: container.Name, Previous: true} + prevPodLogOpts := corev1.PodLogOptions{Container: container.Name, Previous: true, Timestamps: timestamps} previousSuffix := "previous" + if tailLines != nil { + prevPodLogOpts.TailLines = tailLines + } getPodLogOpts(&pod, &prevPodLogOpts, &previousSuffix) } return containerMap, nil @@ -1232,6 +1241,15 @@ func GetFlagIntOrPrompt(cmd *cobra.Command, flag string) int { return val } +// ShowStringPrompt prompts a user to provide a string +func ShowStringPrompt(prompt string) string { + fmt.Printf("%s: ", prompt) + var str string + _, err := fmt.Scan(&str) + Panic(err) + return str +} + // GetFlagStringOrPrompt returns flag value but if empty will prompt to read from stdin func GetFlagStringOrPrompt(cmd *cobra.Command, flag string) string { str, _ := cmd.Flags().GetString(flag)