Repository: stern/stern
Pull request #305 (2024-05-11 11:36:16)

Display different colors for different containers
fixes https://github.com/stern/stern/issues/304
All containers within a pod have the same color by default.
```bash
./stern myapp
```
<img width="854" alt="image" src="https://github.com/stern/stern/assets/40051120/cdea5aa2-a470-4a89-8452-382fd0d77538">

Set `--diff-container` to display different colors for different containers. This is useful when you want to debug logs for multiple containers in the same pod.

```bash
# --diff-container is false by default
./stern myapp --diff-container
```
<img width="833" alt="image" src="https://github.com/stern/stern/assets/40051120/602327b9-480a-42ec-b2d3-d774867c8bed">


Patch:
diff --git a/.gitignore b/.gitignore
index 8ba047e..da819dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
 /dist
 /hack/tools/bin
 vendor
+.idea
+.vscode
diff --git a/README.md b/README.md
index 2b5e815..3fadd29 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,7 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
  `--container`, `-c`         | `.*`                          | Container name when multiple containers in pod. (regular expression)
  `--container-state`         | `all`                         | Tail containers with state in running, waiting, terminated, or all. 'all' matches all container states. To specify multiple states, repeat this or set comma-separated value.
  `--context`                 |                               | The name of the kubeconfig context to use
+ `--diff-container`, `-d`    | `false`                       | Display different colors for different containers.
  `--ephemeral-containers`    | `true`                        | Include or exclude ephemeral containers.
  `--exclude`, `-e`           | `[]`                          | Log lines to exclude. (regular expression)
  `--exclude-container`, `-E` | `[]`                          | Container name to exclude when multiple containers in pod. (regular expression)
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 3257ebb..b8bc8f4 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -86,6 +86,7 @@ type options struct {
 	configFilePath      string
 	showHiddenOptions   bool
 	stdin               bool
+	diffContainer       bool
 
 	client       kubernetes.Interface
 	clientConfig clientcmd.ClientConfig
@@ -318,6 +319,7 @@ func (o *options) sternConfig() (*stern.Config, error) {
 		OnlyLogLines:          o.onlyLogLines,
 		MaxLogRequests:        maxLogRequests,
 		Stdin:                 o.stdin,
+		DiffContainer:         o.diffContainer,
 
 		Out:    o.Out,
 		ErrOut: o.ErrOut,
@@ -435,6 +437,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
 	fs.BoolVarP(&o.version, "version", "v", o.version, "Print the version and exit.")
 	fs.BoolVar(&o.showHiddenOptions, "show-hidden-options", o.showHiddenOptions, "Print a list of hidden options.")
 	fs.BoolVar(&o.stdin, "stdin", o.stdin, "Parse logs from stdin. All Kubernetes related flags are ignored when it is set.")
+	fs.BoolVarP(&o.diffContainer, "diff-container", "d", o.diffContainer, "Display different colors for different containers.")
 
 	fs.Lookup("timestamps").NoOptDefVal = "default"
 }
diff --git a/stern/config.go b/stern/config.go
index 78fd2df..8da802a 100644
--- a/stern/config.go
+++ b/stern/config.go
@@ -51,6 +51,7 @@ type Config struct {
 	OnlyLogLines          bool
 	MaxLogRequests        int
 	Stdin                 bool
+	DiffContainer         bool
 
 	Out    io.Writer
 	ErrOut io.Writer
diff --git a/stern/stern.go b/stern/stern.go
index c1630ef..5c1135c 100644
--- a/stern/stern.go
+++ b/stern/stern.go
@@ -65,7 +65,7 @@ func Run(ctx context.Context, client kubernetes.Interface, config *Config) error
 		}
 	}
 	newTail := func(t *Target) *Tail {
-		return NewTail(client.CoreV1(), t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, newTailOptions())
+		return NewTail(client.CoreV1(), t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, newTailOptions(), config.DiffContainer)
 	}
 
 	if config.Stdin {
diff --git a/stern/tail.go b/stern/tail.go
index 20ab9ed..498816c 100644
--- a/stern/tail.go
+++ b/stern/tail.go
@@ -67,8 +67,8 @@ type ResumeRequest struct {
 }
 
 // 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)
+func NewTail(clientset corev1client.CoreV1Interface, nodeName, namespace, podName, containerName string, tmpl *template.Template, out, errOut io.Writer, options *TailOptions, diffContainer bool) *Tail {
+	podColor, containerColor := determineColor(podName, containerName, diffContainer)
 
 	return &Tail{
 		clientset:      clientset,
@@ -96,15 +96,20 @@ var colorList = [][2]*color.Color{
 	{color.New(color.FgHiRed), color.New(color.FgRed)},
 }
 
-func determineColor(podName string) (podColor, containerColor *color.Color) {
-	hash := fnv.New32()
-	_, _ = hash.Write([]byte(podName))
-	idx := hash.Sum32() % uint32(len(colorList))
-
-	colors := colorList[idx]
+func determineColor(podName, containerName string, diffContainer bool) (podColor, containerColor *color.Color) {
+	colors := colorList[colorIndex(podName)]
+	if diffContainer {
+		return colors[0], colorList[colorIndex(containerName)][1]
+	}
 	return colors[0], colors[1]
 }
 
+func colorIndex(name string) uint32 {
+	hash := fnv.New32()
+	_, _ = hash.Write([]byte(name))
+	return hash.Sum32() % uint32(len(colorList))
+}
+
 // Start starts tailing
 func (t *Tail) Start(ctx context.Context) error {
 	ctx, cancel := context.WithCancel(ctx)


Test patch:
diff --git a/stern/tail_test.go b/stern/tail_test.go
index eb33bcd..f8a9f86 100644
--- a/stern/tail_test.go
+++ b/stern/tail_test.go
@@ -13,8 +13,10 @@ import (
 
 func TestDetermineColor(t *testing.T) {
 	podName := "stern"
-	podColor1, containerColor1 := determineColor(podName)
-	podColor2, containerColor2 := determineColor(podName)
+	containerName := "foo"
+	diffContainer := false
+	podColor1, containerColor1 := determineColor(podName, containerName, diffContainer)
+	podColor2, containerColor2 := determineColor(podName, containerName, diffContainer)
 
 	if podColor1 != podColor2 {
 		t.Errorf("expected color for pod to be the same between invocations but was %v and %v",
@@ -26,6 +28,24 @@ func TestDetermineColor(t *testing.T) {
 	}
 }
 
+func TestDetermineColorDiffContainer(t *testing.T) {
+	podName := "stern"
+	containerName1 := "foo"
+	containerName2 := "bar"
+	diffContainer := true
+	podColor1, containerColor1 := determineColor(podName, containerName1, diffContainer)
+	podColor2, containerColor2 := determineColor(podName, containerName2, diffContainer)
+
+	if podColor1 != podColor2 {
+		t.Errorf("expected color for pod to be the same between invocations but was %v and %v",
+			podColor1, podColor2)
+	}
+	if containerColor1 == containerColor2 {
+		t.Errorf("expected color for container to be different between invocations but was the same: %v",
+			containerColor1)
+	}
+}
+
 func TestConsumeStreamTail(t *testing.T) {
 	logLines := `2023-02-13T21:20:30.000000001Z line 1
 2023-02-13T21:20:30.000000002Z line 2
@@ -83,7 +103,7 @@ line 4 (my-node/my-namespace/my-pod/my-container)
 	for i, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			out := new(bytes.Buffer)
-			tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", tmpl, out, io.Discard, &TailOptions{})
+			tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", tmpl, out, io.Discard, &TailOptions{}, false)
 			tail.resumeRequest = tt.resumeReq
 			if err := tail.ConsumeRequest(context.TODO(), &responseWrapperMock{data: bytes.NewBufferString(logLines)}); err != nil {
 				t.Fatalf("%d: unexpected err %v", i, err)
@@ -142,7 +162,7 @@ func TestPrintStarting(t *testing.T) {
 	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 := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", nil, io.Discard, errOut, tt.options, false)
 		tail.printStarting()
 
 		if !bytes.Equal(tt.expected, errOut.Bytes()) {
@@ -184,7 +204,7 @@ func TestPrintStopping(t *testing.T) {
 	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 := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", nil, io.Discard, errOut, tt.options, false)
 		tail.printStopping()
 
 		if !bytes.Equal(tt.expected, errOut.Bytes()) {