Skip to content

Commit

Permalink
adding a highlight option (#289)
Browse files Browse the repository at this point in the history
* adding a highlight option

Signed-off-by: Prune <prune@lecentre.net>

* merged include and highlights and added tests

Signed-off-by: Prune <prune@lecentre.net>

* replaced -I by -H for highlight option

Signed-off-by: Prune <prune@lecentre.net>

---------

Signed-off-by: Prune <prune@lecentre.net>
  • Loading branch information
prune998 committed Jan 8, 2024
1 parent 1915c58 commit a0436b3
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 5 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# v1.28.0

## :zap: Notable Changes

### Highlight matched strings in the log lines with the highlight option

Some part of a log line can be highlighted while still displaying all other logs lines.

`--highlight` flag now highlight matched strings in the log lines.

```
stern --highlight "\[error\]" .
```


# v1.27.0

## :zap: Notable Changes
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
`--exclude-container`, `-E` | `[]` | Container name to exclude when multiple containers in pod. (regular expression)
`--exclude-pod` | `[]` | Pod name to exclude. (regular expression)
`--field-selector` | | Selector (field query) to filter on. If present, default to ".*" for the pod-query.
`--highlight`, `-H` | `[]` | Log lines to highlight. (regular expression)
`--include`, `-i` | `[]` | Log lines to include. (regular expression)
`--init-containers` | `true` | Include or exclude init containers.
`--kubeconfig` | | Path to the kubeconfig file to use for CLI requests.
Expand Down
8 changes: 8 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type options struct {
namespaces []string
exclude []string
include []string
highlight []string
initContainers bool
ephemeralContainers bool
allNamespaces bool
Expand Down Expand Up @@ -216,6 +217,11 @@ func (o *options) sternConfig() (*stern.Config, error) {
return nil, errors.Wrap(err, "failed to compile regular expression for inclusion filter")
}

highlight, err := compileREs(o.highlight)
if err != nil {
return nil, errors.Wrap(err, "failed to compile regular expression for highlight filter")
}

containerStates := []stern.ContainerState{}
for _, containerStateStr := range makeUnique(o.containerStates) {
containerState, err := stern.NewContainerState(containerStateStr)
Expand Down Expand Up @@ -298,6 +304,7 @@ func (o *options) sternConfig() (*stern.Config, error) {
ContainerStates: containerStates,
Exclude: exclude,
Include: include,
Highlight: highlight,
InitContainers: o.initContainers,
EphemeralContainers: o.ephemeralContainers,
Since: o.since,
Expand Down Expand Up @@ -391,6 +398,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
fs.StringArrayVar(&o.excludePod, "exclude-pod", o.excludePod, "Pod name to exclude. (regular expression)")
fs.BoolVar(&o.noFollow, "no-follow", o.noFollow, "Exit when all logs have been shown.")
fs.StringArrayVarP(&o.include, "include", "i", o.include, "Log lines to include. (regular expression)")
fs.StringArrayVarP(&o.highlight, "highlight", "H", o.highlight, "Log lines to highlight. (regular expression)")
fs.BoolVar(&o.initContainers, "init-containers", o.initContainers, "Include or exclude init containers.")
fs.BoolVar(&o.ephemeralContainers, "ephemeral-containers", o.ephemeralContainers, "Include or exclude ephemeral containers.")
fs.StringSliceVarP(&o.namespaces, "namespace", "n", o.namespaces, "Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.")
Expand Down
15 changes: 15 additions & 0 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ func TestOptionsSternConfig(t *testing.T) {
ContainerStates: []stern.ContainerState{stern.ALL_STATES},
Exclude: nil,
Include: nil,
Highlight: nil,
InitContainers: true,
EphemeralContainers: true,
Since: 48 * time.Hour,
Expand Down Expand Up @@ -503,6 +504,7 @@ func TestOptionsSternConfig(t *testing.T) {
o.containerStates = []string{"running", "terminated"}
o.exclude = []string{"ex1", "ex2"}
o.include = []string{"in1", "in2"}
o.highlight = []string{"hi1", "hi2"}
o.initContainers = false
o.ephemeralContainers = false
o.since = 1 * time.Hour
Expand Down Expand Up @@ -531,6 +533,7 @@ func TestOptionsSternConfig(t *testing.T) {
c.ContainerStates = []stern.ContainerState{stern.RUNNING, stern.TERMINATED}
c.Exclude = []*regexp.Regexp{re("ex1"), re("ex2")}
c.Include = []*regexp.Regexp{re("in1"), re("in2")}
c.Highlight = []*regexp.Regexp{re("hi1"), re("hi2")}
c.InitContainers = false
c.EphemeralContainers = false
c.Since = 1 * time.Hour
Expand Down Expand Up @@ -625,6 +628,7 @@ func TestOptionsSternConfig(t *testing.T) {
o.namespaces = nil
o.exclude = nil
o.include = nil
o.highlight = nil

return o
}(),
Expand Down Expand Up @@ -702,6 +706,17 @@ func TestOptionsSternConfig(t *testing.T) {
nil,
true,
},
{
"error highlight",
func() *options {
o := NewOptions(streams)
o.highlight = []string{"hi1", "[invalid"}

return o
}(),
nil,
true,
},
{
"error containerStates",
func() *options {
Expand Down
1 change: 1 addition & 0 deletions stern/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Config struct {
ContainerStates []ContainerState
Exclude []*regexp.Regexp
Include []*regexp.Regexp
Highlight []*regexp.Regexp
InitContainers bool
EphemeralContainers bool
Since time.Duration
Expand Down
1 change: 1 addition & 0 deletions stern/stern.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func Run(ctx context.Context, client kubernetes.Interface, config *Config) error
SinceSeconds: ptr.To[int64](int64(config.Since.Seconds())),
Exclude: config.Exclude,
Include: config.Include,
Highlight: config.Highlight,
Namespace: config.AllNamespaces || len(namespaces) > 1,
TailLines: config.TailLines,
Follow: config.Follow,
Expand Down
10 changes: 6 additions & 4 deletions stern/tail.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type TailOptions struct {
SinceTime *metav1.Time
Exclude []*regexp.Regexp
Include []*regexp.Regexp
Highlight []*regexp.Regexp
Namespace bool
TailLines *int64
Follow bool
Expand Down Expand Up @@ -113,14 +114,15 @@ func (o TailOptions) IsInclude(msg string) bool {
var colorHighlight = color.New(color.FgRed, color.Bold).SprintFunc()

func (o TailOptions) HighlightMatchedString(msg string) string {
if len(o.Include) == 0 {
highlight := append(o.Include, o.Highlight...)
if len(highlight) == 0 {
return msg
}

if o.reHightlight == nil {
ss := make([]string, len(o.Include))
for i, rin := range o.Include {
ss[i] = rin.String()
ss := make([]string, len(highlight))
for i, hl := range highlight {
ss[i] = hl.String()
}

// We expect a longer match
Expand Down
143 changes: 142 additions & 1 deletion stern/tail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ func TestRemoveSubsecond(t *testing.T) {
}
}

func TestHighlightMatchedString(t *testing.T) {
func TestHighlighIncludedString(t *testing.T) {
tests := []struct {
msg string
include []*regexp.Regexp
Expand Down Expand Up @@ -442,3 +442,144 @@ func TestHighlightMatchedString(t *testing.T) {
}
}
}

func TestIncludeAndHighlightMatchedString(t *testing.T) {
tests := []struct {
msg string
include []*regexp.Regexp
highlight []*regexp.Regexp
expected string
}{
{
"test matched with highlight",
[]*regexp.Regexp{
regexp.MustCompile(`test`),
},
[]*regexp.Regexp{
regexp.MustCompile(`highlight`),
},
"\x1b[31;1mtest\x1b[0m matched with \x1b[31;1mhighlight\x1b[0m",
},
{
"test not-matched",
[]*regexp.Regexp{
regexp.MustCompile(`hoge`),
},
[]*regexp.Regexp{
regexp.MustCompile(`highlight`),
},
"test not-matched",
},
{
"test matched with highlight",
[]*regexp.Regexp{
regexp.MustCompile(`not-matched`),
regexp.MustCompile(`matched`),
},
[]*regexp.Regexp{
regexp.MustCompile(`no-with-highlight`),
regexp.MustCompile(`with highlight`),
},
"test \x1b[31;1mmatched\x1b[0m \x1b[31;1mwith highlight\x1b[0m",
},
{
"test multiple matched with many highlight",
[]*regexp.Regexp{
regexp.MustCompile(`multiple`),
regexp.MustCompile(`matched`),
},
[]*regexp.Regexp{
regexp.MustCompile(`many`),
regexp.MustCompile(`highlight`),
},
"test \x1b[31;1mmultiple\x1b[0m \x1b[31;1mmatched\x1b[0m with \x1b[31;1mmany\x1b[0m \x1b[31;1mhighlight\x1b[0m",
},
{
"test match on the longer one",
[]*regexp.Regexp{
regexp.MustCompile(`match`),
regexp.MustCompile(`match on the longer one`),
},
[]*regexp.Regexp{
regexp.MustCompile(`match`),
regexp.MustCompile(`match on the longer one`),
},
"test \x1b[31;1mmatch on the longer one\x1b[0m",
},
}

orig := color.NoColor
color.NoColor = false
defer func() {
color.NoColor = orig
}()

for i, tt := range tests {
o := &TailOptions{Include: tt.include, Highlight: tt.highlight}
actual := o.HighlightMatchedString(tt.msg)
if actual != tt.expected {
t.Errorf("%d: expected %q, but actual %q", i, tt.expected, actual)
}
}
}

func TestHighlightMatchedString(t *testing.T) {
tests := []struct {
msg string
highlight []*regexp.Regexp
expected string
}{
{
"test matched",
[]*regexp.Regexp{
regexp.MustCompile(`test`),
},
"\x1b[31;1mtest\x1b[0m matched",
},
{
"test not-matched",
[]*regexp.Regexp{
regexp.MustCompile(`hoge`),
},
"test not-matched",
},
{
"test matched",
[]*regexp.Regexp{
regexp.MustCompile(`not-matched`),
regexp.MustCompile(`matched`),
},
"test \x1b[31;1mmatched\x1b[0m",
},
{
"test multiple matched",
[]*regexp.Regexp{
regexp.MustCompile(`multiple`),
regexp.MustCompile(`matched`),
},
"test \x1b[31;1mmultiple\x1b[0m \x1b[31;1mmatched\x1b[0m",
},
{
"test match on the longer one",
[]*regexp.Regexp{
regexp.MustCompile(`match`),
regexp.MustCompile(`match on the longer one`),
},
"test \x1b[31;1mmatch on the longer one\x1b[0m",
},
}

orig := color.NoColor
color.NoColor = false
defer func() {
color.NoColor = orig
}()

for i, tt := range tests {
o := &TailOptions{Highlight: tt.highlight}
actual := o.HighlightMatchedString(tt.msg)
if actual != tt.expected {
t.Errorf("%d: expected %q, but actual %q", i, tt.expected, actual)
}
}
}

0 comments on commit a0436b3

Please sign in to comment.