Skip to content

Commit

Permalink
Add a new "vault monitor" command (#8477)
Browse files Browse the repository at this point in the history
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
4 people authored May 21, 2020
1 parent 399eb35 commit af5338b
Show file tree
Hide file tree
Showing 95 changed files with 1,956 additions and 444 deletions.
4 changes: 4 additions & 0 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
Expand Down Expand Up @@ -65,6 +67,7 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=
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=
Expand All @@ -82,6 +85,7 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
Expand Down
2 changes: 1 addition & 1 deletion api/sys_audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (c *Sys) DisableAudit(path string) error {
return err
}

// Structures for the requests/resposne are all down here. They aren't
// Structures for the requests/response are all down here. They aren't
// individually documented because the map almost directly to the raw HTTP API
// documentation. Please refer to that documentation for more details.

Expand Down
64 changes: 64 additions & 0 deletions api/sys_monitor.go
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
}
2 changes: 1 addition & 1 deletion builtin/credential/okta/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import (
"strings"
"time"

"github.com/hashicorp/go-cleanhttp"
oktaold "github.com/chrismalek/oktasdk-go/okta"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/tokenutil"
"github.com/hashicorp/vault/sdk/logical"
Expand Down
3 changes: 1 addition & 2 deletions command/agent/cf_end_to_end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"time"

hclog "github.com/hashicorp/go-hclog"
log "github.com/hashicorp/go-hclog"
credCF "github.com/hashicorp/vault-plugin-auth-cf"
"github.com/hashicorp/vault-plugin-auth-cf/testing/certificates"
cfAPI "github.com/hashicorp/vault-plugin-auth-cf/testing/cf"
Expand All @@ -29,7 +28,7 @@ func TestCFEndToEnd(t *testing.T) {
coreConfig := &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: log.NewNullLogger(),
Logger: hclog.NewNullLogger(),
CredentialBackends: map[string]logical.Factory{
"cf": credCF.Factory,
},
Expand Down
8 changes: 7 additions & 1 deletion command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/hashicorp/vault/builtin/logical/ssh"
"github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/helper/builtinplugins"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/physical/inmem"
"github.com/hashicorp/vault/vault"
Expand Down Expand Up @@ -84,11 +85,16 @@ func testVaultServerAllBackends(tb testing.TB) (*api.Client, func()) {
// API client, list of unseal keys (as strings), and a closer function.
func testVaultServerUnseal(tb testing.TB) (*api.Client, []string, func()) {
tb.Helper()
logger := log.NewInterceptLogger(&log.LoggerOptions{
Output: log.DefaultOutput,
Level: log.Debug,
JSONFormat: logging.ParseEnvLogFormat() == logging.JSONFormat,
})

return testVaultServerCoreConfig(tb, &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: defaultVaultLogger,
Logger: logger,
CredentialBackends: defaultVaultCredentialBackends,
AuditBackends: defaultVaultAuditBackends,
LogicalBackends: defaultVaultLogicalBackends,
Expand Down
6 changes: 6 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
BaseCommand: getBaseCommand(),
}, nil
},
"monitor": func() (cli.Command, error) {
return &MonitorCommand{
BaseCommand: getBaseCommand(),
ShutdownCh: MakeShutdownCh(),
}, nil
},
}
}

Expand Down
118 changes: 118 additions & 0 deletions command/monitor.go
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
}
}
}
99 changes: 99 additions & 0 deletions command/monitor_test.go
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
})
}
}
Loading

0 comments on commit af5338b

Please sign in to comment.