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

* add logger->log-level str func

* ensure SetLogLevelByName accounts for duplicates

* add read handlers for sys/loggers endpoints

* add changelog entry

* update docs

* ignore base logger

* fix docs formatting issue

* add ReadOperation support to TestSystemBackend_Loggers

* add more robust checks to TestSystemBackend_Loggers

* add more robust checks to TestSystemBackend_LoggersByName

* check for empty name in delete handler
This commit is contained in:
Chris Capurso 2022-11-28 11:18:36 -05:00 committed by GitHub
parent a04855c98d
commit 2843cfcdc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 355 additions and 75 deletions

3
changelog/17979.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
core: Add read support to `sys/loggers` and `sys/loggers/:name` endpoints
```

View File

@ -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.
func ParseLogLevel(logLevel string) (log.Level, error) {
var result log.Level
logLevel = strings.ToLower(strings.TrimSpace(logLevel))
@ -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
}

View File

@ -2866,6 +2866,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()
@ -2874,17 +2875,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
// 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.

View File

@ -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"
@ -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"
@ -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 (
@ -4833,28 +4833,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
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()
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)
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) {
@ -4869,7 +4876,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
}
@ -4880,7 +4887,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
}
@ -4890,12 +4897,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 {
@ -4907,14 +4965,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
@ -4926,14 +4984,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)
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

View File

@ -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.",
@ -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.",

View File

@ -4772,66 +4772,60 @@ func TestProcessLimit(t *testing.T) {
}
}
func validateLevel(level string, logger hclog.Logger) bool {
switch level {
case "trace":
return logger.IsTrace()
case "debug":
return logger.IsDebug()
case "notice", "info", "":
return logger.IsInfo()
case "warn", "warning":
return logger.IsWarn()
case "err", "error":
return logger.IsError()
}
return false
}
func TestSystemBackend_Loggers(t *testing.T) {
testCases := []struct {
level string
expectError bool
level string
expectedLevel string
expectError bool
}{
{
"trace",
"trace",
false,
},
{
"debug",
"debug",
false,
},
{
"notice",
"info",
false,
},
{
"info",
"info",
false,
},
{
"warn",
"warn",
false,
},
{
"warning",
"warn",
false,
},
{
"err",
"error",
false,
},
{
"error",
"error",
false,
},
{
"",
"info",
true,
},
{
"invalid",
"",
true,
},
}
@ -4844,7 +4838,33 @@ func TestSystemBackend_Loggers(t *testing.T) {
core, b, _ := testCoreSystemBackend(t)
// Test core overrides logging level outside of config,
// an initial delete will ensure that we an initial read
// to get expected values is based off of config and not
// the test override that is hidden from this test
req := &logical.Request{
Path: "loggers",
Operation: logical.DeleteOperation,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
req = &logical.Request{
Path: "loggers",
Operation: logical.ReadOperation,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
initialLoggers := resp.Data
req = &logical.Request{
Path: "loggers",
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
@ -4852,7 +4872,7 @@ func TestSystemBackend_Loggers(t *testing.T) {
},
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
respIsError := resp != nil && resp.IsError()
if err != nil || (!tc.expectError && respIsError) {
@ -4864,15 +4884,32 @@ func TestSystemBackend_Loggers(t *testing.T) {
}
if !tc.expectError {
req = &logical.Request{
Path: "loggers",
Operation: logical.ReadOperation,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
for _, logger := range core.allLoggers {
if !validateLevel(tc.level, logger) {
t.Fatalf("expected logger %q to be %q", logger.Name(), tc.level)
loggerName := logger.Name()
levelRaw, ok := resp.Data[loggerName]
if !ok {
t.Errorf("logger %q not found in response", loggerName)
}
if levelStr := levelRaw.(string); levelStr != tc.expectedLevel {
t.Errorf("unexpected level of logger %q, expected: %s, actual: %s", loggerName, tc.expectedLevel, levelStr)
}
}
}
req = &logical.Request{
Path: fmt.Sprintf("loggers"),
Path: "loggers",
Operation: logical.DeleteOperation,
}
@ -4881,9 +4918,29 @@ func TestSystemBackend_Loggers(t *testing.T) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
req = &logical.Request{
Path: "loggers",
Operation: logical.ReadOperation,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
for _, logger := range core.allLoggers {
if !validateLevel(core.logLevel, logger) {
t.Errorf("expected level of logger %q to match original config", logger.Name())
loggerName := logger.Name()
levelRaw, currentOk := resp.Data[loggerName]
initialLevelRaw, initialOk := initialLoggers[loggerName]
if !currentOk || !initialOk {
t.Errorf("logger %q not found", loggerName)
}
levelStr := levelRaw.(string)
initialLevelStr := initialLevelRaw.(string)
if levelStr != initialLevelStr {
t.Errorf("expected level of logger %q to match original config, expected: %s, actual: %s", loggerName, initialLevelStr, levelStr)
}
}
})
@ -4894,78 +4951,91 @@ func TestSystemBackend_LoggersByName(t *testing.T) {
testCases := []struct {
logger string
level string
expectedLevel string
expectWriteError bool
expectDeleteError bool
}{
{
"core",
"trace",
"trace",
false,
false,
},
{
"token",
"debug",
"debug",
false,
false,
},
{
"audit",
"notice",
"info",
false,
false,
},
{
"expiration",
"info",
"info",
false,
false,
},
{
"policy",
"warn",
"warn",
false,
false,
},
{
"activity",
"warning",
"warn",
false,
false,
},
{
"identity",
"err",
"error",
false,
false,
},
{
"rollback",
"error",
"error",
false,
false,
},
{
"system",
"",
"does-not-matter",
true,
false,
},
{
"quotas",
"invalid",
"does-not-matter",
true,
false,
},
{
"",
"info",
"does-not-matter",
true,
true,
},
{
"does_not_exist",
"error",
"does-not-matter",
true,
true,
},
@ -4979,16 +5049,41 @@ func TestSystemBackend_LoggersByName(t *testing.T) {
core, b, _ := testCoreSystemBackend(t)
// Test core overrides logging level outside of config,
// an initial delete will ensure that we an initial read
// to get expected values is based off of config and not
// the test override that is hidden from this test
req := &logical.Request{
Path: "loggers",
Operation: logical.DeleteOperation,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
req = &logical.Request{
Path: "loggers",
Operation: logical.ReadOperation,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
initialLoggers := resp.Data
req = &logical.Request{
Path: fmt.Sprintf("loggers/%s", tc.logger),
Operation: logical.UpdateOperation,
Data: map[string]interface{}{
"name": tc.logger,
"level": tc.level,
},
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
respIsError := resp != nil && resp.IsError()
if err != nil || (!tc.expectWriteError && respIsError) {
@ -5000,13 +5095,34 @@ func TestSystemBackend_LoggersByName(t *testing.T) {
}
if !tc.expectWriteError {
req = &logical.Request{
Path: "loggers",
Operation: logical.ReadOperation,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
for _, logger := range core.allLoggers {
if logger.Name() != tc.logger && !validateLevel(core.logLevel, logger) {
t.Errorf("expected level of logger %q to be unchanged", logger.Name())
loggerName := logger.Name()
levelRaw, currentOk := resp.Data[loggerName]
initialLevelRaw, initialOk := initialLoggers[loggerName]
if !currentOk || !initialOk {
t.Errorf("logger %q not found", loggerName)
}
if !validateLevel(tc.level, logger) {
t.Fatalf("expected logger %q to be %q", logger.Name(), tc.level)
levelStr := levelRaw.(string)
initialLevelStr := initialLevelRaw.(string)
if loggerName == tc.logger && levelStr != tc.expectedLevel {
t.Fatalf("expected logger %q to be %q, actual: %s", loggerName, tc.expectedLevel, levelStr)
}
if loggerName != tc.logger && levelStr != initialLevelStr {
t.Errorf("expected level of logger %q to be unchanged, exepcted: %s, actual: %s", loggerName, initialLevelStr, levelStr)
}
}
}
@ -5014,9 +5130,6 @@ func TestSystemBackend_LoggersByName(t *testing.T) {
req = &logical.Request{
Path: fmt.Sprintf("loggers/%s", tc.logger),
Operation: logical.DeleteOperation,
Data: map[string]interface{}{
"name": tc.logger,
},
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
@ -5031,10 +5144,28 @@ func TestSystemBackend_LoggersByName(t *testing.T) {
}
if !tc.expectDeleteError {
for _, logger := range core.allLoggers {
if !validateLevel(core.logLevel, logger) {
t.Errorf("expected level of logger %q to match original config", logger.Name())
}
req = &logical.Request{
Path: fmt.Sprintf("loggers/%s", tc.logger),
Operation: logical.ReadOperation,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("unexpected error, err: %v, resp: %#v", err, resp)
}
currentLevel, ok := resp.Data[tc.logger].(string)
if !ok {
t.Fatalf("expected resp to include %q, resp: %#v", tc.logger, resp)
}
initialLevel, ok := initialLoggers[tc.logger].(string)
if !ok {
t.Fatalf("expected initial loggers to include %q, resp: %#v", tc.logger, initialLoggers)
}
if currentLevel != initialLevel {
t.Errorf("expected level of logger %q to match original config, expected: %s, actual: %s", tc.logger, initialLevel, currentLevel)
}
}
})

View File

@ -8,8 +8,8 @@ description: The `/sys/loggers` endpoint is used modify the verbosity level of l
The `/sys/loggers` endpoint is used modify the verbosity level of logging.
!> **NOTE:** Changes made to the log level using this endpoint are not persisted and will be restored
to either the default log level (info) or the level specified using `log_level` in vault.hcl or the `VAULT_LOG_LEVEL`
!> **NOTE:** Changes made to the log level using this endpoint are not persisted and will be restored
to either the default log level (info) or the level specified using `log_level` in vault.hcl or the `VAULT_LOG_LEVEL`
environment variable once the Vault service is reloaded or restarted.
## Modify verbosity level of all loggers
@ -71,6 +71,52 @@ $ curl \
http://127.0.0.1:8200/v1/sys/loggers/core
```
## Read verbosity level of all loggers
| Method | Path |
| :----- | :------------- |
| `GET` | `/sys/loggers` |
### Sample Request
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
https://127.0.0.1:8200/v1/sys/loggers
```
### Sample Response
```json
{
"audit": "trace",
"core": "info",
"policy": "debug"
}
```
## Read verbosity level of a single logger
| Method | Path |
| :----- | :------------------- |
| `GET` | `/sys/loggers/:name` |
### Sample Request
```shell-session
$ curl \
--header "X-Vault-Token: ..." \
https://127.0.0.1:8200/v1/sys/loggers/core
```
### Sample Response
```json
{
"core": "info"
}
```
## Revert verbosity of all loggers to configured level
| Method | Path |