Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VAULT-9427: Add read support to sys/loggers endpoints #17979

Merged
merged 11 commits into from
Nov 28, 2022
3 changes: 3 additions & 0 deletions changelog/17979.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
core: Add read support to `sys/loggers` and `sys/loggers/:name` endpoints
```
23 changes: 23 additions & 0 deletions helper/logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ func ParseLogFormat(format string) (LogFormat, error) {
}
}

// ParseLogLevel returns the hclog.Level that corresponds with the provided level string.
// This differs hclog.LevelFromString in that it supports additional level strings.
Copy link

@peteski22 peteski22 Nov 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I took this code and refactored it into a function when adding the log file feature, so this is my bad and thanks for adding comments!

However, I think that the func should probably not include notice or warning as that hasn't (to my knowledge) been documented on our site.

There's also differing info depending on whether you look at the docs for the server command or Vault configuration.

Trace, Debug, Error, Warn, Info. vs trace, debug, info, warn, *err*.

My PR I have open at the moment to add rotation to log files has updates to the docs to make them all standardised, and I will also update ParseLogLevel to not handle notice, warning or "" which defaults to info.

func ParseLogLevel(logLevel string) (log.Level, error) {
var result log.Level
logLevel = strings.ToLower(strings.TrimSpace(logLevel))
Expand All @@ -133,3 +135,24 @@ func ParseLogLevel(logLevel string) (log.Level, error) {

return result, nil
}

// TranslateLoggerLevel returns the string that corresponds with logging level of the hclog.Logger.
func TranslateLoggerLevel(logger log.Logger) (string, error) {
var result string

if logger.IsTrace() {
result = "trace"
} else if logger.IsDebug() {
result = "debug"
} else if logger.IsInfo() {
result = "info"
} else if logger.IsWarn() {
result = "warn"
} else if logger.IsError() {
result = "error"
} else {
return "", fmt.Errorf("unknown log level")
}

return result, nil
}
12 changes: 9 additions & 3 deletions vault/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -2860,6 +2860,7 @@ func (c *Core) AddLogger(logger log.Logger) {
c.allLoggers = append(c.allLoggers, logger)
}

// SetLogLevel sets logging level for all tracked loggers to the level provided
func (c *Core) SetLogLevel(level log.Level) {
c.allLoggersLock.RLock()
defer c.allLoggersLock.RUnlock()
Expand All @@ -2868,17 +2869,22 @@ func (c *Core) SetLogLevel(level log.Level) {
}
}

func (c *Core) SetLogLevelByName(name string, level log.Level) error {
// SetLogLevelByName sets the logging level of named logger to level provided
ccapurso marked this conversation as resolved.
Show resolved Hide resolved
// if it exists. Core.allLoggers is a slice and as such it is entirely possible
// that multiple entries exist for the same name. Each instance will be modified.
func (c *Core) SetLogLevelByName(name string, level log.Level) bool {
c.allLoggersLock.RLock()
defer c.allLoggersLock.RUnlock()

found := false
for _, logger := range c.allLoggers {
if logger.Name() == name {
logger.SetLevel(level)
return nil
found = true
}
}

return fmt.Errorf("logger %q does not exist", name)
return found
}

// SetConfig sets core's config object to the newly provided config.
Expand Down
129 changes: 96 additions & 33 deletions vault/logical_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ import (
"time"
"unicode"

"github.com/hashicorp/vault/helper/versions"
"golang.org/x/crypto/sha3"

"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
Expand All @@ -32,10 +29,12 @@ import (
semver "github.com/hashicorp/go-version"
"github.com/hashicorp/vault/helper/hostutil"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/logging"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/monitor"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/random"
"github.com/hashicorp/vault/helper/versions"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
Expand All @@ -44,6 +43,7 @@ import (
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/version"
"github.com/mitchellh/mapstructure"
"golang.org/x/crypto/sha3"
)

const (
Expand Down Expand Up @@ -4828,28 +4828,35 @@ func (b *SystemBackend) handleVersionHistoryList(ctx context.Context, req *logic
return logical.ListResponseWithInfo(respKeys, respKeyInfo), nil
}

// getLogLevel returns the hclog.Level that corresponds with the provided level string.
// This differs hclog.LevelFromString in that it supports additional level strings so
// that in remains consistent with the handling found in the "vault server" command.
func getLogLevel(logLevel string) (log.Level, error) {
var level log.Level

switch logLevel {
case "trace":
level = log.Trace
case "debug":
level = log.Debug
case "notice", "info", "":
level = log.Info
case "warn", "warning":
level = log.Warn
case "err", "error":
level = log.Error
default:
return level, fmt.Errorf("unrecognized log level %q", logLevel)
func (b *SystemBackend) handleLoggersRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
b.Core.allLoggersLock.RLock()
defer b.Core.allLoggersLock.RUnlock()

loggers := make(map[string]interface{})
warnings := make([]string, 0)

for _, logger := range b.Core.allLoggers {
loggerName := logger.Name()

// ignore base logger
if loggerName == "" {
continue
}

logLevel, err := logging.TranslateLoggerLevel(logger)
if err != nil {
warnings = append(warnings, fmt.Sprintf("cannot translate level for %q: %s", loggerName, err.Error()))
} else {
loggers[loggerName] = logLevel
}
}

return level, nil
resp := &logical.Response{
Data: loggers,
Warnings: warnings,
}

return resp, nil
}

func (b *SystemBackend) handleLoggersWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
Expand All @@ -4864,7 +4871,7 @@ func (b *SystemBackend) handleLoggersWrite(ctx context.Context, req *logical.Req
return logical.ErrorResponse("level is empty"), nil
}

level, err := getLogLevel(logLevel)
level, err := logging.ParseLogLevel(logLevel)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("invalid level provided: %s", err.Error())), nil
}
Expand All @@ -4875,7 +4882,7 @@ func (b *SystemBackend) handleLoggersWrite(ctx context.Context, req *logical.Req
}

func (b *SystemBackend) handleLoggersDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
level, err := getLogLevel(b.Core.logLevel)
level, err := logging.ParseLogLevel(b.Core.logLevel)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("log level from config is invalid: %s", err.Error())), nil
}
Expand All @@ -4885,12 +4892,63 @@ func (b *SystemBackend) handleLoggersDelete(ctx context.Context, req *logical.Re
return nil, nil
}

func (b *SystemBackend) handleLoggersByNameRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
nameRaw, nameOk := d.GetOk("name")
if !nameOk {
return logical.ErrorResponse("name is required"), nil
}

name := nameRaw.(string)
if name == "" {
return logical.ErrorResponse("name is empty"), nil
}

b.Core.allLoggersLock.RLock()
defer b.Core.allLoggersLock.RUnlock()

loggers := make(map[string]interface{})
warnings := make([]string, 0)

for _, logger := range b.Core.allLoggers {
loggerName := logger.Name()

// ignore base logger
if loggerName == "" {
continue
}

if loggerName == name {
logLevel, err := logging.TranslateLoggerLevel(logger)

if err != nil {
warnings = append(warnings, fmt.Sprintf("cannot translate level for %q: %s", loggerName, err.Error()))
} else {
loggers[loggerName] = logLevel
}

break
}
}

resp := &logical.Response{
Data: loggers,
Warnings: warnings,
}

return resp, nil
}

func (b *SystemBackend) handleLoggersByNameWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
nameRaw, nameOk := d.GetOk("name")
if !nameOk {
return logical.ErrorResponse("name is required"), nil
}

name := nameRaw.(string)
if name == "" {
return logical.ErrorResponse("name is empty"), nil
}

logLevelRaw, logLevelOk := d.GetOk("level")

if !logLevelOk {
Expand All @@ -4902,14 +4960,14 @@ func (b *SystemBackend) handleLoggersByNameWrite(ctx context.Context, req *logic
return logical.ErrorResponse("level is empty"), nil
}

level, err := getLogLevel(logLevel)
level, err := logging.ParseLogLevel(logLevel)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("invalid level provided: %s", err.Error())), nil
}

err = b.Core.SetLogLevelByName(nameRaw.(string), level)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("invalid params: %s", err.Error())), nil
success := b.Core.SetLogLevelByName(name, level)
if !success {
return logical.ErrorResponse(fmt.Sprintf("logger %q not found", name)), nil
}

return nil, nil
Expand All @@ -4921,14 +4979,19 @@ func (b *SystemBackend) handleLoggersByNameDelete(ctx context.Context, req *logi
return logical.ErrorResponse("name is required"), nil
}

level, err := getLogLevel(b.Core.logLevel)
level, err := logging.ParseLogLevel(b.Core.logLevel)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("log level from config is invalid: %s", err.Error())), nil
}

err = b.Core.SetLogLevelByName(nameRaw.(string), level)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("invalid params: %s", err.Error())), nil
name := nameRaw.(string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Do we need to check if name is an empty string here the same as above functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should. Thanks!

if name == "" {
return logical.ErrorResponse("name is empty"), nil
}

success := b.Core.SetLogLevelByName(name, level)
if !success {
return logical.ErrorResponse(fmt.Sprintf("logger %q not found", name)), nil
}

return nil, nil
Expand Down
8 changes: 8 additions & 0 deletions vault/logical_system_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ func (b *SystemBackend) configPaths() []*framework.Path {
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.handleLoggersRead,
Summary: "Read the log level for all existing loggers.",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.handleLoggersWrite,
Summary: "Modify the log level for all existing loggers.",
Expand All @@ -322,6 +326,10 @@ func (b *SystemBackend) configPaths() []*framework.Path {
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.handleLoggersByNameRead,
Summary: "Read the log level for a single logger.",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.handleLoggersByNameWrite,
Summary: "Modify the log level of a single logger.",
Expand Down
Loading