Skip to content

Commit

Permalink
Add --stdin for stdin log parsing (#292)
Browse files Browse the repository at this point in the history
* Add --stdin flag

* Move Log and TailOptions to separate file

When Log and TailOptions are used by other classes it is now more
obvious that they're not hard lined to Tail.

* Create FileTail for tailing log files

FileTail is heavily based on Tail but I saw no obvious way to abstract
the common parts of the classes to not duplicate them. FileTail is so
small it's hardly noticeable anyway.

* Create stdin tailing when using --stdin

* Document --stdin flag

* Remove start/stop printing from FileTail

Also moving file tailing to main thread as there are no parallel tails
with that config.
  • Loading branch information
hogklint committed Mar 29, 2024
1 parent 9763d95 commit 53fc746
Show file tree
Hide file tree
Showing 11 changed files with 611 additions and 438 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `d
`--selector`, `-l` | | Selector (label query) to filter on. If present, default to ".*" for the pod-query.
`--show-hidden-options` | `false` | Print a list of hidden options.
`--since`, `-s` | `48h0m0s` | Return logs newer than a relative duration like 5s, 2m, or 3h.
`--stdin` | `false` | Parse logs from stdin. All Kubernetes related flags are ignored when it is set.
`--tail` | `-1` | The number of lines from the end of the logs to show. Defaults to -1, showing all logs.
`--template` | | Template to use for log lines, leave empty to use --output flag.
`--template-file`, `-T` | | Path to template to use for log lines, leave empty to use --output flag. It overrides --template option.
Expand Down Expand Up @@ -313,6 +314,12 @@ Output log lines only:
stern . --only-log-lines
```

Read from stdin:

```
stern --stdin < service.log
```

## Completion

Stern supports command-line auto completion for bash, zsh or fish. `stern
Expand Down
7 changes: 5 additions & 2 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ type options struct {
node string
configFilePath string
showHiddenOptions bool
stdin bool

client kubernetes.Interface
clientConfig clientcmd.ClientConfig
Expand Down Expand Up @@ -154,8 +155,8 @@ func (o *options) Complete(args []string) error {
}

func (o *options) Validate() error {
if !o.prompt && o.podQuery == "" && o.resource == "" && o.selector == "" && o.fieldSelector == "" {
return errors.New("One of pod-query, --selector, --field-selector or --prompt is required")
if !o.prompt && o.podQuery == "" && o.resource == "" && o.selector == "" && o.fieldSelector == "" && !o.stdin {
return errors.New("One of pod-query, --selector, --field-selector, --prompt or --stdin is required")
}
if o.selector != "" && o.resource != "" {
return errors.New("--selector and the <resource>/<name> query can not be set at the same time")
Expand Down Expand Up @@ -317,6 +318,7 @@ func (o *options) sternConfig() (*stern.Config, error) {
Resource: o.resource,
OnlyLogLines: o.onlyLogLines,
MaxLogRequests: maxLogRequests,
Stdin: o.stdin,

Out: o.Out,
ErrOut: o.ErrOut,
Expand Down Expand Up @@ -419,6 +421,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
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.")
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.Lookup("timestamps").NoOptDefVal = "default"
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func TestOptionsValidate(t *testing.T) {
{
"No required options",
NewOptions(streams),
"One of pod-query, --selector, --field-selector or --prompt is required",
"One of pod-query, --selector, --field-selector, --prompt or --stdin is required",
},
{
"Specify both selector and resource",
Expand Down
1 change: 1 addition & 0 deletions stern/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type Config struct {
Resource string
OnlyLogLines bool
MaxLogRequests int
Stdin bool

Out io.Writer
ErrOut io.Writer
Expand Down
89 changes: 89 additions & 0 deletions stern/file_tail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package stern

import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"text/template"

"github.com/fatih/color"
)

type FileTail struct {
Options *TailOptions
tmpl *template.Template
in io.Reader
out io.Writer
errOut io.Writer
}

// NewFileTail returns a new tail of the input reader
func NewFileTail(tmpl *template.Template, in io.Reader, out, errOut io.Writer, options *TailOptions) *FileTail {
return &FileTail{
Options: options,
tmpl: tmpl,
in: in,
out: out,
errOut: errOut,
}
}

// Start starts tailing
func (t *FileTail) Start() error {
reader := bufio.NewReader(t.in)
err := t.ConsumeReader(reader)

return err
}

// ConsumeReader reads the data from the reader and writes into the out
// writer.
func (t *FileTail) ConsumeReader(reader *bufio.Reader) error {
for {
line, err := reader.ReadBytes('\n')
if len(line) != 0 {
t.consumeLine(strings.TrimSuffix(string(line), "\n"))
}

if err != nil {
if err != io.EOF {
return err
}
return nil
}
}
}

// Print prints a color coded log message
func (t *FileTail) Print(msg string) {
vm := Log{
Message: msg,
NodeName: "",
Namespace: "",
PodName: "",
ContainerName: "",
PodColor: color.New(color.Reset),
ContainerColor: color.New(color.Reset),
}

var buf bytes.Buffer
if err := t.tmpl.Execute(&buf, vm); err != nil {
fmt.Fprintf(t.errOut, "expanding template failed: %s\n", err)
return
}

fmt.Fprint(t.out, buf.String())
}

func (t *FileTail) consumeLine(line string) {
content := line

if t.Options.IsExclude(content) || !t.Options.IsInclude(content) {
return
}

msg := t.Options.HighlightMatchedString(content)
t.Print(msg)
}
47 changes: 47 additions & 0 deletions stern/file_tail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package stern

import (
"bufio"
"bytes"
"io"
"strings"
"testing"
"text/template"
)

func TestConsumeFileTail(t *testing.T) {
logLines := `line 1
line 2
line 3
line 4`
tmpl := template.Must(template.New("").Parse(`{{printf "%s\n" .Message}}`))

tests := []struct {
name string
resumeReq *ResumeRequest
expected []byte
}{
{
name: "normal",
expected: []byte(`line 1
line 2
line 3
line 4
`),
},
}

for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := new(bytes.Buffer)
tail := NewFileTail(tmpl, nil, out, io.Discard, &TailOptions{})
if err := tail.ConsumeReader(bufio.NewReader(strings.NewReader(logLines))); err != nil {
t.Fatalf("%d: unexpected err %v", i, err)
}

if !bytes.Equal(tt.expected, out.Bytes()) {
t.Errorf("%d: expected %s, but actual %s", i, tt.expected, out)
}
})
}
}
40 changes: 25 additions & 15 deletions stern/stern.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package stern
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -48,6 +49,30 @@ func Run(ctx context.Context, client kubernetes.Interface, config *Config) error
}
}

newTailOptions := func() *TailOptions {
return &TailOptions{
Timestamps: config.Timestamps,
TimestampFormat: config.TimestampFormat,
Location: config.Location,
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,
OnlyLogLines: config.OnlyLogLines,
}
}
newTail := func(t *Target) *Tail {
return NewTail(client.CoreV1(), t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, newTailOptions())
}

if config.Stdin {
tail := NewFileTail(config.Template, os.Stdin, config.Out, config.ErrOut, newTailOptions())
return tail.Start()
}

var resource struct {
kind string
name string
Expand Down Expand Up @@ -78,21 +103,6 @@ func Run(ctx context.Context, client kubernetes.Interface, config *Config) error
ephemeralContainers: config.EphemeralContainers,
containerStates: config.ContainerStates,
})
newTail := func(t *Target) *Tail {
return NewTail(client.CoreV1(), t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, &TailOptions{
Timestamps: config.Timestamps,
TimestampFormat: config.TimestampFormat,
Location: config.Location,
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,
OnlyLogLines: config.OnlyLogLines,
})
}

if !config.Follow {
var eg errgroup.Group
Expand Down

0 comments on commit 53fc746

Please sign in to comment.