Repository: stern/stern
Pull request #216 (2023-01-27 13:24:01)

Add --only-log-lines flag that prints only log lines
This PR adds a new flag `--only-log-lines` that prints only log lines.

```
$ stern . --tail=1
+ nginx-cfbcb7b98-29wn7 › nginx
+ nginx-cfbcb7b98-96xsv › nginx
nginx-cfbcb7b98-96xsv nginx 2023/01/27 13:20:48 [notice] 1#1: start worker process 46
nginx-cfbcb7b98-29wn7 nginx 2023/01/27 13:20:45 [notice] 1#1: start worker process 46
^C

$ stern . --tail=1 --only-log-lines
nginx-cfbcb7b98-96xsv nginx 2023/01/27 13:20:48 [notice] 1#1: start worker process 46
nginx-cfbcb7b98-29wn7 nginx 2023/01/27 13:20:45 [notice] 1#1: start worker process 46
^C
```

/ref https://github.com/stern/stern/discussions/202


Patch:
diff --git a/README.md b/README.md
index bb7cd52..9ff0808 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,7 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
  `--kubeconfig`              |           | Path to kubeconfig file to use. Default to KUBECONFIG variable then ~/.kube/config path.
  `--namespace`, `-n`         |           | Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.
  `--no-follow`               | `false`   | Exit when all logs have been shown.
+ `--only-log-lines`          | `false`   | Print only log lines
  `--output`, `-o`            | `default` | Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson]
  `--prompt`, `-p`            | `false`   | Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values.
  `--selector`, `-l`          |           | Selector (label query) to filter on. If present, default to ".*" for the pod-query.
@@ -237,6 +238,12 @@ Trigger the interactive prompt to select an 'app.kubernetes.io/instance' label v
 stern -p
 ```
 
+Output log lines only:
+
+```
+stern . --only-log-lines
+```
+
 ## Completion
 
 Stern supports command-line auto completion for bash, zsh or fish. `stern
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 34beae4..2457692 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -68,6 +68,7 @@ type options struct {
 	noFollow            bool
 	resource            string
 	verbosity           int
+	onlyLogLines        bool
 }
 
 func NewOptions(streams genericclioptions.IOStreams) *options {
@@ -331,6 +332,7 @@ func (o *options) sternConfig() (*stern.Config, error) {
 		Template:              template,
 		Follow:                !o.noFollow,
 		Resource:              o.resource,
+		OnlyLogLines:          o.onlyLogLines,
 
 		Out:    o.Out,
 		ErrOut: o.ErrOut,
@@ -378,6 +380,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
 	fs.StringVar(&o.template, "template", o.template, "Template to use for log lines, leave empty to use --output flag.")
 	fs.BoolVarP(&o.timestamps, "timestamps", "t", o.timestamps, "Print timestamps.")
 	fs.StringVar(&o.timezone, "timezone", o.timezone, "Set timestamps to specific timezone.")
+	fs.BoolVar(&o.onlyLogLines, "only-log-lines", o.onlyLogLines, "Print only log lines")
 	fs.IntVar(&o.verbosity, "verbosity", o.verbosity, "Number of the log level verbosity")
 	fs.BoolVarP(&o.version, "version", "v", o.version, "Print the version and exit.")
 }
diff --git a/stern/config.go b/stern/config.go
index 3e75a8e..45d3a2c 100644
--- a/stern/config.go
+++ b/stern/config.go
@@ -48,6 +48,7 @@ type Config struct {
 	Template              *template.Template
 	Follow                bool
 	Resource              string
+	OnlyLogLines          bool
 
 	Out    io.Writer
 	ErrOut io.Writer
diff --git a/stern/stern.go b/stern/stern.go
index 14fa1b8..13c5051 100644
--- a/stern/stern.go
+++ b/stern/stern.go
@@ -121,6 +121,7 @@ func Run(ctx context.Context, config *Config) error {
 			Namespace:    config.AllNamespaces || len(namespaces) > 1,
 			TailLines:    config.TailLines,
 			Follow:       config.Follow,
+			OnlyLogLines: config.OnlyLogLines,
 		})
 	}
 
diff --git a/stern/tail.go b/stern/tail.go
index bb2cda2..c9644eb 100644
--- a/stern/tail.go
+++ b/stern/tail.go
@@ -60,6 +60,7 @@ type TailOptions struct {
 	Namespace    bool
 	TailLines    *int64
 	Follow       bool
+	OnlyLogLines bool
 }
 
 func (o TailOptions) IsExclude(msg string) bool {
@@ -107,18 +108,23 @@ func (o TailOptions) UpdateTimezoneIfNeeded(message string) (string, error) {
 
 // NewTail returns a new tail for a Kubernetes container inside a pod
 func NewTail(clientset corev1client.CoreV1Interface, nodeName, namespace, podName, containerName string, tmpl *template.Template, out, errOut io.Writer, options *TailOptions) *Tail {
+	podColor, containerColor := determineColor(podName)
+
 	return &Tail{
-		clientset:     clientset,
-		NodeName:      nodeName,
-		Namespace:     namespace,
-		PodName:       podName,
-		ContainerName: containerName,
-		Options:       options,
-		closed:        make(chan struct{}),
-		tmpl:          tmpl,
-		active:        true,
-		out:           out,
-		errOut:        errOut,
+		clientset:      clientset,
+		NodeName:       nodeName,
+		Namespace:      namespace,
+		PodName:        podName,
+		ContainerName:  containerName,
+		Options:        options,
+		closed:         make(chan struct{}),
+		tmpl:           tmpl,
+		active:         true,
+		podColor:       podColor,
+		containerColor: containerColor,
+
+		out:    out,
+		errOut: errOut,
 	}
 }
 
@@ -142,22 +148,13 @@ func determineColor(podName string) (podColor, containerColor *color.Color) {
 
 // Start starts tailing
 func (t *Tail) Start(ctx context.Context) error {
-	t.podColor, t.containerColor = determineColor(t.PodName)
-
 	ctx, cancel := context.WithCancel(ctx)
 	go func() {
 		<-t.closed
 		cancel()
 	}()
 
-	g := color.New(color.FgHiGreen, color.Bold).SprintFunc()
-	p := t.podColor.SprintFunc()
-	c := t.containerColor.SprintFunc()
-	if t.Options.Namespace {
-		fmt.Fprintf(t.errOut, "%s %s %s › %s\n", g("+"), p(t.Namespace), p(t.PodName), c(t.ContainerName))
-	} else {
-		fmt.Fprintf(t.errOut, "%s %s › %s\n", g("+"), p(t.PodName), c(t.ContainerName))
-	}
+	t.printStarting()
 
 	req := t.clientset.Pods(t.Namespace).GetLogs(t.PodName, &corev1.PodLogOptions{
 		Follow:       t.Options.Follow,
@@ -179,18 +176,37 @@ func (t *Tail) Start(ctx context.Context) error {
 
 // Close stops tailing
 func (t *Tail) Close() {
-	r := color.New(color.FgHiRed, color.Bold).SprintFunc()
-	p := t.podColor.SprintFunc()
-	c := t.containerColor.SprintFunc()
-	if t.Options.Namespace {
-		fmt.Fprintf(t.errOut, "%s %s %s › %s\n", r("-"), p(t.Namespace), p(t.PodName), c(t.ContainerName))
-	} else {
-		fmt.Fprintf(t.errOut, "%s %s › %s\n", r("-"), p(t.PodName), c(t.ContainerName))
-	}
+	t.printStopping()
 
 	close(t.closed)
 }
 
+func (t *Tail) printStarting() {
+	if !t.Options.OnlyLogLines {
+		g := color.New(color.FgHiGreen, color.Bold).SprintFunc()
+		p := t.podColor.SprintFunc()
+		c := t.containerColor.SprintFunc()
+		if t.Options.Namespace {
+			fmt.Fprintf(t.errOut, "%s %s %s › %s\n", g("+"), p(t.Namespace), p(t.PodName), c(t.ContainerName))
+		} else {
+			fmt.Fprintf(t.errOut, "%s %s › %s\n", g("+"), p(t.PodName), c(t.ContainerName))
+		}
+	}
+}
+
+func (t *Tail) printStopping() {
+	if !t.Options.OnlyLogLines {
+		r := color.New(color.FgHiRed, color.Bold).SprintFunc()
+		p := t.podColor.SprintFunc()
+		c := t.containerColor.SprintFunc()
+		if t.Options.Namespace {
+			fmt.Fprintf(t.errOut, "%s %s %s › %s\n", r("-"), p(t.Namespace), p(t.PodName), c(t.ContainerName))
+		} else {
+			fmt.Fprintf(t.errOut, "%s %s › %s\n", r("-"), p(t.PodName), c(t.ContainerName))
+		}
+	}
+}
+
 // ConsumeRequest reads the data from request and writes into the out
 // writer.
 func (t *Tail) ConsumeRequest(ctx context.Context, request rest.ResponseWrapper) error {


Test patch:
diff --git a/stern/tail_test.go b/stern/tail_test.go
index 340b14d..7a43fb8 100644
--- a/stern/tail_test.go
+++ b/stern/tail_test.go
@@ -197,3 +197,87 @@ func (r *responseWrapperMock) DoRaw(context.Context) ([]byte, error) {
 func (r *responseWrapperMock) Stream(context.Context) (io.ReadCloser, error) {
 	return io.NopCloser(r.data), nil
 }
+
+func TestPrintStarting(t *testing.T) {
+	tests := []struct {
+		options  *TailOptions
+		expected []byte
+	}{
+		{
+			&TailOptions{},
+			[]byte("+ my-pod › my-container\n"),
+		},
+		{
+			&TailOptions{
+				Namespace: true,
+			},
+			[]byte("+ my-namespace my-pod › my-container\n"),
+		},
+		{
+			&TailOptions{
+				OnlyLogLines: true,
+			},
+			[]byte{},
+		},
+		{
+			&TailOptions{
+				Namespace:    true,
+				OnlyLogLines: true,
+			},
+			[]byte{},
+		},
+	}
+
+	clientset := fake.NewSimpleClientset()
+	for i, tt := range tests {
+		errOut := new(bytes.Buffer)
+		tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", nil, io.Discard, errOut, tt.options)
+		tail.printStarting()
+
+		if !bytes.Equal(tt.expected, errOut.Bytes()) {
+			t.Errorf("%d: expected %q, but actual %q", i, tt.expected, errOut)
+		}
+	}
+}
+
+func TestPrintStopping(t *testing.T) {
+	tests := []struct {
+		options  *TailOptions
+		expected []byte
+	}{
+		{
+			&TailOptions{},
+			[]byte("- my-pod › my-container\n"),
+		},
+		{
+			&TailOptions{
+				Namespace: true,
+			},
+			[]byte("- my-namespace my-pod › my-container\n"),
+		},
+		{
+			&TailOptions{
+				OnlyLogLines: true,
+			},
+			[]byte{},
+		},
+		{
+			&TailOptions{
+				Namespace:    true,
+				OnlyLogLines: true,
+			},
+			[]byte{},
+		},
+	}
+
+	clientset := fake.NewSimpleClientset()
+	for i, tt := range tests {
+		errOut := new(bytes.Buffer)
+		tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", nil, io.Discard, errOut, tt.options)
+		tail.printStopping()
+
+		if !bytes.Equal(tt.expected, errOut.Bytes()) {
+			t.Errorf("%d: expected %q, but actual %q", i, tt.expected, errOut)
+		}
+	}
+}