Skip to content

Commit

Permalink
cli: rework audit log trace and add error log trace
Browse files Browse the repository at this point in the history
This commit does two things:
 - Add error log tracing via the `/v1/log/error/trace` API
 - Rework the audit log tracing CLI UI.

Now, the log event stream to STDOUT is displayed as a table:
```
┌──────────┬─────────────┬─────────────┬─────────────────────────────────────────┬──────────┐
│   Time   │  Identity   │   Status    │ API Operations                          │ Response │
├──────────┼─────────────┼─────────────┼─────────────────────────────────────────┼──────────┤
│ 13:43:48 │ 3ecfcdf38f… │   200 OK    │ /v1/log/audit/trace                     │  282µs   │
│ 15:08:06 │ 3ecfcdf38f… │   200 OK    │ /v1/key/generate/my-minio-key           │  550µs   │
└──────────┴─────────────┴─────────────┴─────────────────────────────────────────┴──────────┘
```

A user can specify whether the error or the audit log should be traced
via the `--type=[audit|error]` flag. The default is `--type=audit` which
is aligned with the previous behavior.

If a user wants to trace error and audit logs at the same time (e.g.
with `--json` flag) the following bash/zsh command can be used:
```
{ kes log trace --type=error --json & kes log trace --type=audit --json; } | jq
```

The current table CLI output requires that the terminal supports unicode
characters - which is the case for modern terminals, including windows.
  • Loading branch information
Andreas Auernhammer authored and harshavardhana committed Aug 11, 2020
1 parent 477b32b commit a75c6ec
Show file tree
Hide file tree
Showing 6 changed files with 525 additions and 42 deletions.
232 changes: 193 additions & 39 deletions cmd/kes/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ package main
import (
"flag"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"runtime"
"syscall"
"strings"
"time"

"github.com/fatih/color"
"github.com/minio/kes"
"github.com/minio/kes/internal/xterm"

ui "github.com/gizak/termui/v3"
)

const logCmdUsage = `usage: %s <command>
Expand Down Expand Up @@ -52,6 +56,11 @@ Connects to a KES server and traces log events.
usage: %s [flags]
--type Specify the log event type. (default: audit)
Valid options are:
--type=audit
--type=error
--json Print log events as JSON.
-k, --insecure Skip X.509 certificate validation during TLS handshake.
Expand All @@ -65,8 +74,13 @@ func logTrace(args []string) error {
fmt.Fprintf(cli.Output(), logTraceCmdUsage, cli.Name())
}

var jsonOutput bool
var insecureSkipVerify bool
var (
typeFlag string
jsonOutput bool

insecureSkipVerify bool
)
cli.StringVar(&typeFlag, "type", "audit", "Log event type [ audit | error ]")
cli.BoolVar(&jsonOutput, "json", false, "Print log events as JSON")
cli.BoolVar(&insecureSkipVerify, "k", false, "Skip X.509 certificate validation during TLS handshake")
cli.BoolVar(&insecureSkipVerify, "insecure", false, "Skip X.509 certificate validation during TLS handshake")
Expand All @@ -79,60 +93,200 @@ func logTrace(args []string) error {
if err != nil {
return err
}
stream, err := client.TraceAuditLog()
if err != nil {

switch strings.ToLower(typeFlag) {
case "audit":
stream, err := client.TraceAuditLog()
if err != nil {
return err
}
defer stream.Close()

if !isTerm(os.Stdout) || jsonOutput {
closeOn(stream, os.Interrupt, os.Kill)
for stream.Next() {
fmt.Println(string(stream.Bytes()))
}
return stream.Err()
}
return traceAuditLogWithUI(stream)
case "error":
stream, err := client.TraceErrorLog()
if err != nil {
return err
}
defer stream.Close()

if !isTerm(os.Stdout) || jsonOutput {
closeOn(stream, os.Interrupt, os.Kill)
for stream.Next() {
fmt.Println(string(stream.Bytes()))
}
return stream.Err()
}
return traceErrorLogWithUI(stream)
default:
return fmt.Errorf("Unknown log event type: '%s'", typeFlag)
}
}

// traceAuditLogWithUI iterates over the audit log
// event stream and prints a table-like UI to STDOUT.
//
// Each event is displayed as a new row and the UI is
// automatically adjusted to the terminal window size.
func traceAuditLogWithUI(stream *kes.AuditStream) error {
table := xterm.NewTable("Time", "Identity", "Status", "API Operations", "Response")
table.Header()[0].Width = 0.12
table.Header()[1].Width = 0.15
table.Header()[2].Width = 0.15
table.Header()[3].Width = 0.45
table.Header()[4].Width = 0.12

table.Header()[0].Alignment = xterm.AlignCenter
table.Header()[1].Alignment = xterm.AlignCenter
table.Header()[2].Alignment = xterm.AlignCenter
table.Header()[3].Alignment = xterm.AlignLeft
table.Header()[4].Alignment = xterm.AlignCenter

// Initialize the terminal UI and listen on resize
// events and Ctrl-C / Escape key events.
if err := ui.Init(); err != nil {
return err
}
defer stream.Close()
defer table.Draw() // Draw the table AFTER closing the UI one more time.
defer ui.Close() // Closing the UI cleans the screen.

sigCh := make(chan os.Signal)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
if err := stream.Close(); err != nil {
fmt.Fprintln(cli.Output(), err)
events := ui.PollEvents()
for {
switch event := <-events; {
case event.Type == ui.ResizeEvent:
table.Draw()
case event.ID == "<C-c>" || event.ID == "<Escape>":
if err := stream.Close(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
return
}
}
}()

isTerminal := isTerm(os.Stdout)
var (
green = color.New(color.FgGreen)
red = color.New(color.FgRed)
)
table.Draw()
for stream.Next() {
if !isTerminal || jsonOutput {
fmt.Println(string(stream.Bytes()))
continue
}

event := stream.Event()
identity := event.Request.Identity
if len(identity) > 7 {
identity = identity[:7]
}
hh, mm, ss := event.Time.Clock()

var status string
if runtime.GOOS == "windows" { // don't colorize on windows
status = fmt.Sprintf("[%d %s]", event.Response.StatusCode, http.StatusText(event.Response.StatusCode))
var (
identity = xterm.NewCell(event.Request.Identity)
status = xterm.NewCell(fmt.Sprintf("%d %s", event.Response.StatusCode, http.StatusText(event.Response.StatusCode)))
path = xterm.NewCell(event.Request.Path)
reqTime = xterm.NewCell(fmt.Sprintf("%02d:%02d:%02d", hh, mm, ss))
respTime *xterm.Cell
)
if event.Response.StatusCode == http.StatusOK {
status.Color = green
} else {
identity = color.YellowString(identity)
if event.Response.StatusCode == http.StatusOK {
status = color.GreenString("[%d %s]", event.Response.StatusCode, http.StatusText(event.Response.StatusCode))
} else {
status = color.RedString("[%d %s]", event.Response.StatusCode, http.StatusText(event.Response.StatusCode))
}
status.Color = red
}

// Truncate duration values such that we show reasonable
// time values - like 1.05s or 345.76ms.
respTime := event.Response.Time
switch {
case respTime >= time.Second:
respTime = respTime.Truncate(10 * time.Millisecond)
case respTime >= time.Millisecond:
respTime = respTime.Truncate(10 * time.Microsecond)
case event.Response.Time >= time.Second:
respTime = xterm.NewCell(event.Response.Time.Truncate(10 * time.Millisecond).String())
case event.Response.Time >= time.Millisecond:
respTime = xterm.NewCell(event.Response.Time.Truncate(10 * time.Microsecond).String())
default:
respTime = respTime.Truncate(time.Microsecond)
respTime = xterm.NewCell(event.Response.Time.Truncate(time.Microsecond).String())
}

const format = "%s %s %-25s %10s\n"
fmt.Printf(format, identity, status, event.Request.Path, respTime)
table.AddRow(reqTime, identity, status, path, respTime)
table.Draw()
}
return stream.Err()
}

// traceErrorLogWithUI iterates over the error log
// event stream and prints a table-like UI to STDOUT.
//
// Each event is displayed as a new row and the UI is
// automatically adjusted to the terminal window size.
func traceErrorLogWithUI(stream *kes.ErrorStream) error {
table := xterm.NewTable("Time", "Error")
table.Header()[0].Width = 0.12
table.Header()[1].Width = 0.87

table.Header()[0].Alignment = xterm.AlignCenter
table.Header()[1].Alignment = xterm.AlignLeft

// Initialize the terminal UI and listen on resize
// events and Ctrl-C / Escape key events.
if err := ui.Init(); err != nil {
return err
}
defer table.Draw() // Draw the table AFTER closing the UI one more time.
defer ui.Close() // Closing the UI cleans the screen.

go func() {
events := ui.PollEvents()
for {
switch event := <-events; {
case event.Type == ui.ResizeEvent:
table.Draw()
case event.ID == "<C-c>" || event.ID == "<Escape>":
if err := stream.Close(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
return
}
}
}()

table.Draw()
for stream.Next() {
// An error event message has the following form: YY/MM/DD hh/mm/ss <message>.
// We split this message into 3 segements:
// 1. YY/MM/DD
// 2. hh/mm/ss
// 3. <message>
// The 2nd segment is the day-time and 3rd segment is the actual error message.
// We replace any '\n' with a whitespace to avoid multi-line table rows.
segements := strings.SplitN(stream.Event().Message, " ", 3)
var (
message *xterm.Cell
reqTime *xterm.Cell
)
if len(segements) == 3 {
message = xterm.NewCell(strings.ReplaceAll(segements[2], "\n", " "))
reqTime = xterm.NewCell(segements[1])
} else {
hh, mm, ss := time.Now().Clock()

message = xterm.NewCell(strings.ReplaceAll(stream.Event().Message, "\n", " "))
reqTime = xterm.NewCell(fmt.Sprintf("%02d:%02d:%02d", hh, mm, ss))
}
table.AddRow(reqTime, message)
table.Draw()
}
return stream.Err()
}

// closeOn closes c if one of the given system signals
// occurs. If c.Close() returns an error this error is
// written to STDERR.
func closeOn(c io.Closer, signals ...os.Signal) {
sigCh := make(chan os.Signal)
signal.Notify(sigCh, signals...)

go func() {
<-sigCh
if err := c.Close(); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}()
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ go 1.13
require (
github.com/aws/aws-sdk-go v1.26.3
github.com/fatih/color v1.7.0
github.com/gizak/termui/v3 v3.1.0
github.com/hashicorp/vault/api v1.0.4
github.com/kr/pretty v0.1.0 // indirect
github.com/secure-io/sio-go v0.3.0
github.com/stretchr/testify v1.4.0 // indirect
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/sys v0.0.0-20190412213103-97732733099d
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.4
Expand Down
13 changes: 11 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down Expand Up @@ -64,16 +66,22 @@ github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRU
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
Expand All @@ -93,9 +101,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo=
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f h1:kz4KIr+xcPUsI3VMoqWfPMvtnJ6MGfiVwsWSVzphMO4=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
Expand Down

0 comments on commit a75c6ec

Please sign in to comment.