Skip to content

Commit

Permalink
Backport: Add read support to sys/loggers endpoints (#18161)
Browse files Browse the repository at this point in the history
* add initial logging helper package

* 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

* add logfile

* remove doc changes
  • Loading branch information
ccapurso authored Dec 6, 2022
1 parent 2d3fdce commit 463c3d8
Show file tree
Hide file tree
Showing 7 changed files with 499 additions and 74 deletions.
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
```
56 changes: 56 additions & 0 deletions helper/logging/logfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package logging

import (
"os"
"path/filepath"
"strings"
"sync"
)

type LogFile struct {
// Name of the log file
fileName string

// Path to the log file
logPath string

// fileInfo is the pointer to the current file being written to
fileInfo *os.File

// acquire is the mutex utilized to ensure we have no concurrency issues
acquire sync.Mutex
}

func NewLogFile(logPath string, fileName string) *LogFile {
return &LogFile{
fileName: strings.TrimSpace(fileName),
logPath: strings.TrimSpace(logPath),
}
}

// Write is used to implement io.Writer
func (l *LogFile) Write(b []byte) (n int, err error) {
l.acquire.Lock()
defer l.acquire.Unlock()
// Create a new file if we have no file to write to
if l.fileInfo == nil {
if err := l.openNew(); err != nil {
return 0, err
}
}

return l.fileInfo.Write(b)
}

func (l *LogFile) openNew() error {
newFilePath := filepath.Join(l.logPath, l.fileName)

// Try to open an existing file or create a new one if it doesn't exist.
filePointer, err := os.OpenFile(newFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o640)
if err != nil {
return err
}

l.fileInfo = filePointer
return nil
}
158 changes: 158 additions & 0 deletions helper/logging/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package logging

import (
"errors"
"fmt"
"io"
"path/filepath"
"strings"

log "github.com/hashicorp/go-hclog"
)

const (
UnspecifiedFormat LogFormat = iota
StandardFormat
JSONFormat
)

type LogFormat int

// LogConfig should be used to supply configuration when creating a new Vault logger
type LogConfig struct {
name string
logLevel log.Level
logFormat LogFormat
logFilePath string
}

func NewLogConfig(name string, logLevel log.Level, logFormat LogFormat, logFilePath string) LogConfig {
return LogConfig{
name: name,
logLevel: logLevel,
logFormat: logFormat,
logFilePath: strings.TrimSpace(logFilePath),
}
}

func (c LogConfig) IsFormatJson() bool {
return c.logFormat == JSONFormat
}

// Stringer implementation
func (lf LogFormat) String() string {
switch lf {
case UnspecifiedFormat:
return "unspecified"
case StandardFormat:
return "standard"
case JSONFormat:
return "json"
}

// unreachable
return "unknown"
}

// noErrorWriter is a wrapper to suppress errors when writing to w.
type noErrorWriter struct {
w io.Writer
}

func (w noErrorWriter) Write(p []byte) (n int, err error) {
_, _ = w.w.Write(p)
// We purposely return n == len(p) as if write was successful
return len(p), nil
}

// Setup creates a new logger with the specified configuration and writer
func Setup(config LogConfig, w io.Writer) (log.InterceptLogger, error) {
// Validate the log level
if config.logLevel.String() == "unknown" {
return nil, fmt.Errorf("invalid log level: %v", config.logLevel)
}

// If out is os.Stdout and Vault is being run as a Windows Service, writes will
// fail silently, which may inadvertently prevent writes to other writers.
// noErrorWriter is used as a wrapper to suppress any errors when writing to out.
writers := []io.Writer{noErrorWriter{w: w}}

if config.logFilePath != "" {
dir, fileName := filepath.Split(config.logFilePath)
if fileName == "" {
fileName = "vault-agent.log"
}
logFile := NewLogFile(dir, fileName)
if err := logFile.openNew(); err != nil {
return nil, fmt.Errorf("failed to set up file logging: %w", err)
}
writers = append(writers, logFile)
}

logger := log.NewInterceptLogger(&log.LoggerOptions{
Name: config.name,
Level: config.logLevel,
Output: io.MultiWriter(writers...),
JSONFormat: config.IsFormatJson(),
})
return logger, nil
}

// ParseLogFormat parses the log format from the provided string.
func ParseLogFormat(format string) (LogFormat, error) {
switch strings.ToLower(strings.TrimSpace(format)) {
case "":
return UnspecifiedFormat, nil
case "standard":
return StandardFormat, nil
case "json":
return JSONFormat, nil
default:
return UnspecifiedFormat, fmt.Errorf("unknown log format: %s", format)
}
}

// 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))

switch logLevel {
case "trace":
result = log.Trace
case "debug":
result = log.Debug
case "notice", "info", "":
result = log.Info
case "warn", "warning":
result = log.Warn
case "err", "error":
result = log.Error
default:
return -1, errors.New(fmt.Sprintf("unknown log level: %s", logLevel))
}

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
// 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
Loading

0 comments on commit 463c3d8

Please sign in to comment.