-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Part of #251 @TarantoolBot document Title: `tt log command` This patch adds new `tt log` command. By default this command prints last 10 lines of all instances logs. A user can specify an application or instance name and limit the number of lines using the `--lines` option. With the `--follow` flag `tt log` prints logs as the log files grow.
- Loading branch information
Showing
10 changed files
with
994 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package cmd | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"os/signal" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/tarantool/tt/cli/cmd/internal" | ||
"github.com/tarantool/tt/cli/cmdcontext" | ||
"github.com/tarantool/tt/cli/modules" | ||
"github.com/tarantool/tt/cli/running" | ||
"github.com/tarantool/tt/cli/tail" | ||
"github.com/tarantool/tt/cli/util" | ||
) | ||
|
||
var logOpts struct { | ||
nLines int // How many lines to print. | ||
follow bool // Follow logs output. | ||
} | ||
|
||
// NewLogCmd creates log command. | ||
func NewLogCmd() *cobra.Command { | ||
var logCmd = &cobra.Command{ | ||
Use: "log [<APP_NAME> | <APP_NAME:INSTANCE_NAME>] [flags]", | ||
Short: `Get logs of instance(s)`, | ||
Run: func(cmd *cobra.Command, args []string) { | ||
cmdCtx.CommandName = cmd.Name() | ||
err := modules.RunCmd(&cmdCtx, cmd.CommandPath(), &modulesInfo, | ||
internalLogModule, args) | ||
util.HandleCmdErr(cmd, err) | ||
}, | ||
ValidArgsFunction: func( | ||
cmd *cobra.Command, | ||
args []string, | ||
toComplete string) ([]string, cobra.ShellCompDirective) { | ||
return internal.ValidArgsFunction( | ||
cliOpts, &cmdCtx, cmd, toComplete, | ||
running.ExtractAppNames, | ||
running.ExtractInstanceNames) | ||
}, | ||
} | ||
|
||
logCmd.Flags().IntVarP(&logOpts.nLines, "lines", "n", 10, | ||
"Count of last lines to output") | ||
logCmd.Flags().BoolVarP(&logOpts.follow, "follow", "f", false, | ||
"Output appended data as the log file grows") | ||
|
||
return logCmd | ||
} | ||
|
||
func printLines(ctx context.Context, in <-chan string) error { | ||
for { | ||
select { | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
case line, ok := <-in: | ||
if !ok { | ||
return nil | ||
} | ||
fmt.Println(line) | ||
} | ||
} | ||
} | ||
|
||
func follow(instances []running.InstanceCtx, n int) error { | ||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | ||
defer stop() | ||
|
||
nextColor := tail.DefaultColorPicker() | ||
color := nextColor() | ||
const logLinesChannelCapacity = 64 | ||
logLines := make(chan string, logLinesChannelCapacity) | ||
tailRoutinesStarted := 0 | ||
for _, inst := range instances { | ||
if err := tail.Follow(ctx, logLines, | ||
tail.NewLogFormatter(running.GetAppInstanceName(inst)+": ", color), | ||
inst.Log, n); err != nil { | ||
if errors.Is(err, os.ErrNotExist) { | ||
continue | ||
} | ||
stop() | ||
return fmt.Errorf("cannot read log file %q: %s", inst.Log, err) | ||
} | ||
tailRoutinesStarted++ | ||
color = nextColor() | ||
} | ||
|
||
if tailRoutinesStarted > 0 { | ||
return printLines(ctx, logLines) | ||
} | ||
return nil | ||
} | ||
|
||
func printLastN(instances []running.InstanceCtx, n int) error { | ||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | ||
defer stop() | ||
|
||
nextColor := tail.DefaultColorPicker() | ||
color := nextColor() | ||
for _, inst := range instances { | ||
logLines, err := tail.TailN(ctx, | ||
tail.NewLogFormatter(running.GetAppInstanceName(inst)+": ", color), inst.Log, n) | ||
if err != nil { | ||
if errors.Is(err, os.ErrNotExist) { | ||
continue | ||
} | ||
stop() | ||
return fmt.Errorf("cannot read log file %q: %s", inst.Log, err) | ||
} | ||
if err := printLines(ctx, logLines); err != nil { | ||
return err | ||
} | ||
color = nextColor() | ||
} | ||
return nil | ||
} | ||
|
||
// internalLogModule is a default log module. | ||
func internalLogModule(cmdCtx *cmdcontext.CmdCtx, args []string) error { | ||
if !isConfigExist(cmdCtx) { | ||
return errNoConfig | ||
} | ||
|
||
var err error | ||
var runningCtx running.RunningCtx | ||
if err = running.FillCtx(cliOpts, cmdCtx, &runningCtx, args); err != nil { | ||
return err | ||
} | ||
|
||
if logOpts.follow { | ||
return follow(runningCtx.Instances, logOpts.nLines) | ||
} | ||
|
||
return printLastN(runningCtx.Instances, logOpts.nLines) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package tail | ||
|
||
import "github.com/fatih/color" | ||
|
||
// ColorPicker returns a color. | ||
type ColorPicker func() color.Color | ||
|
||
// DefaultColorPicker create a color picker to get a color from a default colors set. | ||
func DefaultColorPicker() ColorPicker { | ||
var colorTable = []color.Color{ | ||
*color.New(color.FgCyan), | ||
*color.New(color.FgGreen), | ||
*color.New(color.FgMagenta), | ||
*color.New(color.FgYellow), | ||
*color.New(color.FgBlue), | ||
} | ||
|
||
i := 0 | ||
return func() color.Color { | ||
color := colorTable[i] | ||
i = (i + 1) % len(colorTable) | ||
return color | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package tail_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/fatih/color" | ||
"github.com/tarantool/tt/cli/tail" | ||
) | ||
|
||
func TestDefaultColorPicker(t *testing.T) { | ||
expectedColors := []color.Color{ | ||
*color.New(color.FgCyan), | ||
*color.New(color.FgGreen), | ||
*color.New(color.FgMagenta), | ||
*color.New(color.FgYellow), | ||
*color.New(color.FgBlue), | ||
} | ||
|
||
colorPicker := tail.DefaultColorPicker() | ||
for i := 0; i < 10; i++ { | ||
got := colorPicker() | ||
got.Equals(&expectedColors[i%len(expectedColors)]) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
package tail | ||
|
||
import ( | ||
"bufio" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"strings" | ||
|
||
"github.com/fatih/color" | ||
"github.com/nxadm/tail" | ||
) | ||
|
||
const blockSize = 8192 | ||
|
||
// LogFormatter is a function used to format log string before output. | ||
type LogFormatter func(str string) string | ||
|
||
// NewLogFormatter creates a function to make log prefix colored. | ||
func NewLogFormatter(prefix string, color color.Color) LogFormatter { | ||
buf := strings.Builder{} | ||
buf.Grow(512) | ||
return func(str string) string { | ||
buf.Reset() | ||
color.Fprint(&buf, prefix) | ||
buf.WriteString(str) | ||
return buf.String() | ||
} | ||
} | ||
|
||
// newTailReader return a reader for last count lines. | ||
func newTailReader(ctx context.Context, reader io.ReadSeeker, count int) (io.Reader, int64, error) { | ||
end, err := reader.Seek(0, io.SeekEnd) | ||
if err != nil { | ||
return nil, 0, err | ||
} | ||
|
||
if count <= 0 { | ||
return &io.LimitedReader{R: reader, N: 0}, end, nil | ||
} | ||
|
||
startPos := end | ||
// Skip last char because it can be new-line. For example, tail reader for 'line\n' and n==1 | ||
// should not count last \n as a line. | ||
readOffset := end - 1 | ||
|
||
buf := make([]byte, blockSize) | ||
linesFound := 0 | ||
for readOffset != 0 && linesFound != count { | ||
|
||
select { | ||
case <-ctx.Done(): | ||
return nil, 0, ctx.Err() | ||
default: | ||
} | ||
|
||
limitedReader := io.LimitedReader{R: reader, N: int64(len(buf))} | ||
readOffset -= limitedReader.N | ||
if readOffset < 0 { | ||
limitedReader.N += readOffset | ||
readOffset = 0 | ||
} | ||
readOffset, err = reader.Seek(readOffset, io.SeekStart) | ||
if err != nil { | ||
return nil, 0, err | ||
} | ||
readBytes, err := limitedReader.Read(buf) | ||
if err != nil && !errors.Is(err, io.EOF) { | ||
return nil, startPos, fmt.Errorf("failed to read: %s", err) | ||
} | ||
for i := readBytes - 1; i > 0; i-- { | ||
if buf[i] == '\n' { | ||
// In case of \n\n\n bytes, start position should not be moved one byte forward. | ||
if startPos-(readOffset+int64(i)) == 1 { | ||
startPos = readOffset + int64(i) | ||
} else { | ||
startPos = readOffset + int64(i) + 1 | ||
} | ||
|
||
linesFound++ | ||
if linesFound == count { | ||
break | ||
} | ||
} | ||
} | ||
} | ||
if linesFound == count { | ||
reader.Seek(startPos, io.SeekStart) | ||
return &io.LimitedReader{R: reader, N: end - startPos}, startPos, nil | ||
} | ||
reader.Seek(0, io.SeekStart) | ||
return &io.LimitedReader{R: reader, N: end}, 0, nil | ||
} | ||
|
||
// TailN calls sends last n lines of the file to the channel. | ||
func TailN(ctx context.Context, logFormatter LogFormatter, fileName string, | ||
n int) (<-chan string, error) { | ||
if n < 0 { | ||
return nil, fmt.Errorf("negative lines count is not supported") | ||
} | ||
|
||
file, err := os.Open(fileName) | ||
if err != nil { | ||
return nil, fmt.Errorf("cannot open %q: %w", fileName, err) | ||
} | ||
|
||
reader, _, err := newTailReader(ctx, file, n) | ||
if err != nil { | ||
file.Close() | ||
return nil, err | ||
} | ||
|
||
scanner := bufio.NewScanner(reader) | ||
out := make(chan string, 8) | ||
go func() { | ||
defer close(out) | ||
defer file.Close() | ||
for scanner.Scan() { | ||
select { | ||
case <-ctx.Done(): | ||
return | ||
case out <- logFormatter(scanner.Text()): | ||
} | ||
} | ||
}() | ||
return out, nil | ||
} | ||
|
||
// Follow sends to the channel each new line from the file as it grows. | ||
func Follow(ctx context.Context, out chan<- string, logFormatter LogFormatter, fileName string, | ||
n int) error { | ||
file, err := os.Open(fileName) | ||
if err != nil { | ||
return fmt.Errorf("cannot open %q: %w", fileName, err) | ||
} | ||
defer file.Close() | ||
|
||
_, startPos, err := newTailReader(ctx, file, n) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
t, err := tail.TailFile(fileName, tail.Config{ | ||
Location: &tail.SeekInfo{ | ||
Offset: startPos, | ||
Whence: io.SeekStart, | ||
}, | ||
MustExist: true, | ||
Follow: true, | ||
ReOpen: true, | ||
CompleteLines: false, | ||
Logger: tail.DiscardingLogger}) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
go func() { | ||
for { | ||
select { | ||
case <-ctx.Done(): | ||
t.Stop() | ||
t.Wait() | ||
return | ||
case line := <-t.Lines: | ||
out <- logFormatter(line.Text) | ||
} | ||
} | ||
}() | ||
return nil | ||
} |
Oops, something went wrong.