diff --git a/changelog/17979.txt b/changelog/17979.txt new file mode 100644 index 000000000..81a5c023c --- /dev/null +++ b/changelog/17979.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core: Add read support to `sys/loggers` and `sys/loggers/:name` endpoints +``` diff --git a/helper/logging/logger.go b/helper/logging/logger.go index ec42ef0e1..6e87fde53 100644 --- a/helper/logging/logger.go +++ b/helper/logging/logger.go @@ -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 +} diff --git a/vault/core.go b/vault/core.go index a31a925d8..6eb473553 100644 --- a/vault/core.go +++ b/vault/core.go @@ -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. diff --git a/vault/logical_system.go b/vault/logical_system.go index c10d7e82d..8305383fe 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -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 diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index fbeb9d541..4bd65b959 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -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.", diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index e7fe7fa73..633b4c8a2 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -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) } } }) diff --git a/website/content/api-docs/system/loggers.mdx b/website/content/api-docs/system/loggers.mdx index 73d2c53ef..364770140 100644 --- a/website/content/api-docs/system/loggers.mdx +++ b/website/content/api-docs/system/loggers.mdx @@ -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 |