-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a new "vault monitor" command (#8477)
Add a new "vault monitor" command Co-authored-by: ncabatoff <ncabatoff@hashicorp.com> Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com> Co-authored-by: Jeff Mitchell <jeffrey.mitchell@gmail.com>
- Loading branch information
1 parent
399eb35
commit af5338b
Showing
95 changed files
with
1,956 additions
and
444 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
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,64 @@ | ||
package api | ||
|
||
import ( | ||
"bufio" | ||
"context" | ||
"fmt" | ||
) | ||
|
||
// Monitor returns a channel that outputs strings containing the log messages | ||
// coming from the server. | ||
func (c *Sys) Monitor(ctx context.Context, logLevel string) (chan string, error) { | ||
r := c.c.NewRequest("GET", "/v1/sys/monitor") | ||
|
||
if logLevel == "" { | ||
r.Params.Add("log_level", "info") | ||
} else { | ||
r.Params.Add("log_level", logLevel) | ||
} | ||
|
||
resp, err := c.c.RawRequestWithContext(ctx, r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
logCh := make(chan string, 64) | ||
|
||
go func() { | ||
scanner := bufio.NewScanner(resp.Body) | ||
droppedCount := 0 | ||
|
||
defer close(logCh) | ||
defer resp.Body.Close() | ||
|
||
for { | ||
if ctx.Err() != nil { | ||
return | ||
} | ||
|
||
if !scanner.Scan() { | ||
return | ||
} | ||
|
||
logMessage := scanner.Text() | ||
|
||
if droppedCount > 0 { | ||
select { | ||
case logCh <- fmt.Sprintf("Monitor dropped %d logs during monitor request\n", droppedCount): | ||
droppedCount = 0 | ||
default: | ||
droppedCount++ | ||
continue | ||
} | ||
} | ||
|
||
select { | ||
case logCh <- logMessage: | ||
default: | ||
droppedCount++ | ||
} | ||
} | ||
}() | ||
|
||
return logCh, nil | ||
} |
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
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
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,118 @@ | ||
package command | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/hashicorp/vault/sdk/helper/strutil" | ||
"github.com/mitchellh/cli" | ||
"github.com/posener/complete" | ||
) | ||
|
||
var _ cli.Command = (*MonitorCommand)(nil) | ||
var _ cli.CommandAutocomplete = (*MonitorCommand)(nil) | ||
|
||
type MonitorCommand struct { | ||
*BaseCommand | ||
|
||
logLevel string | ||
|
||
// ShutdownCh is used to capture interrupt signal and end streaming | ||
ShutdownCh chan struct{} | ||
} | ||
|
||
func (c *MonitorCommand) Synopsis() string { | ||
return "Stream log messages from a Vault server" | ||
} | ||
|
||
func (c *MonitorCommand) Help() string { | ||
helpText := ` | ||
Usage: vault monitor [options] | ||
Stream log messages of a Vault server. The monitor command lets you listen | ||
for log levels that may be filtered out of the server logs. For example, | ||
the server may be logging at the INFO level, but with the monitor command | ||
you can set -log-level=DEBUG. | ||
` + c.Flags().Help() | ||
|
||
return strings.TrimSpace(helpText) | ||
} | ||
|
||
func (c *MonitorCommand) Flags() *FlagSets { | ||
set := c.flagSet(FlagSetHTTP) | ||
|
||
f := set.NewFlagSet("Monitor Options") | ||
f.StringVar(&StringVar{ | ||
Name: "log-level", | ||
Target: &c.logLevel, | ||
Default: "info", | ||
Completion: complete.PredictSet("trace", "debug", "info", "warn", "error"), | ||
Usage: "If passed, the log level to monitor logs. Supported values" + | ||
"(in order of detail) are \"trace\", \"debug\", \"info\", \"warn\"" + | ||
" and \"error\". These are not case sensitive.", | ||
}) | ||
|
||
return set | ||
} | ||
|
||
func (c *MonitorCommand) AutocompleteArgs() complete.Predictor { | ||
return complete.PredictNothing | ||
} | ||
|
||
func (c *MonitorCommand) AutocompleteFlags() complete.Flags { | ||
return c.Flags().Completions() | ||
} | ||
|
||
func (c *MonitorCommand) Run(args []string) int { | ||
f := c.Flags() | ||
|
||
if err := f.Parse(args); err != nil { | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
parsedArgs := f.Args() | ||
if len(parsedArgs) > 0 { | ||
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(parsedArgs))) | ||
return 1 | ||
} | ||
|
||
c.logLevel = strings.ToLower(c.logLevel) | ||
validLevels := []string{"trace", "debug", "info", "warn", "error"} | ||
if !strutil.StrListContains(validLevels, c.logLevel) { | ||
c.UI.Error(fmt.Sprintf("%s is an unknown log level. Valid log levels are: %s", c.logLevel, validLevels)) | ||
return 1 | ||
} | ||
|
||
client, err := c.Client() | ||
if err != nil { | ||
c.UI.Error(err.Error()) | ||
return 2 | ||
} | ||
|
||
// Remove the default 60 second timeout so we can stream indefinitely | ||
client.SetClientTimeout(0) | ||
|
||
var logCh chan string | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
logCh, err = client.Sys().Monitor(ctx, c.logLevel) | ||
if err != nil { | ||
c.UI.Error(fmt.Sprintf("Error starting monitor: %s", err)) | ||
return 1 | ||
} | ||
|
||
for { | ||
select { | ||
case log, ok := <-logCh: | ||
if !ok { | ||
return 0 | ||
} | ||
c.UI.Info(log) | ||
case <-c.ShutdownCh: | ||
return 0 | ||
} | ||
} | ||
} |
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,99 @@ | ||
package command | ||
|
||
import ( | ||
"strings" | ||
"sync/atomic" | ||
"testing" | ||
"time" | ||
|
||
"github.com/hashicorp/vault/helper/testhelpers" | ||
"github.com/mitchellh/cli" | ||
) | ||
|
||
func testMonitorCommand(tb testing.TB) (*cli.MockUi, *MonitorCommand) { | ||
tb.Helper() | ||
|
||
ui := cli.NewMockUi() | ||
return ui, &MonitorCommand{ | ||
BaseCommand: &BaseCommand{ | ||
UI: ui, | ||
}, | ||
} | ||
} | ||
|
||
func TestMonitorCommand_Run(t *testing.T) { | ||
t.Parallel() | ||
|
||
cases := []struct { | ||
name string | ||
args []string | ||
out string | ||
code int64 | ||
}{ | ||
{ | ||
"valid", | ||
[]string{ | ||
"-log-level=debug", | ||
}, | ||
"", | ||
0, | ||
}, | ||
{ | ||
"too_many_args", | ||
[]string{ | ||
"-log-level=debug", | ||
"foo", | ||
}, | ||
"Too many arguments", | ||
1, | ||
}, | ||
{ | ||
"unknown_log_level", | ||
[]string{ | ||
"-log-level=haha", | ||
}, | ||
"haha is an unknown log level", | ||
1, | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
tc := tc | ||
|
||
t.Run(tc.name, func(t *testing.T) { | ||
t.Parallel() | ||
client, closer := testVaultServer(t) | ||
defer closer() | ||
|
||
var code int64 | ||
shutdownCh := make(chan struct{}) | ||
|
||
ui, cmd := testMonitorCommand(t) | ||
cmd.client = client | ||
cmd.ShutdownCh = shutdownCh | ||
|
||
stopCh := testhelpers.GenerateDebugLogs(t, client) | ||
|
||
go func() { | ||
atomic.StoreInt64(&code, int64(cmd.Run(tc.args))) | ||
}() | ||
|
||
select { | ||
case <-time.After(3 * time.Second): | ||
stopCh <- struct{}{} | ||
close(shutdownCh) | ||
} | ||
|
||
if atomic.LoadInt64(&code) != tc.code { | ||
t.Errorf("expected %d to be %d", code, tc.code) | ||
} | ||
|
||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String() | ||
if !strings.Contains(combined, tc.out) { | ||
t.Fatalf("expected %q to contain %q", combined, tc.out) | ||
} | ||
|
||
<-stopCh | ||
}) | ||
} | ||
} |
Oops, something went wrong.