224 lines
6.4 KiB
Go
224 lines
6.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package logging
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-multierror"
|
|
)
|
|
|
|
const (
|
|
UnspecifiedFormat LogFormat = iota
|
|
StandardFormat
|
|
JSONFormat
|
|
)
|
|
|
|
// defaultRotateDuration is the default time taken by the agent to rotate logs
|
|
const defaultRotateDuration = 24 * time.Hour
|
|
|
|
type LogFormat int
|
|
|
|
// LogConfig should be used to supply configuration when creating a new Vault logger
|
|
type LogConfig struct {
|
|
// Name is the name the returned logger will use to prefix log lines.
|
|
Name string
|
|
|
|
// LogLevel is the minimum level to be logged.
|
|
LogLevel hclog.Level
|
|
|
|
// LogFormat is the log format to use, supported formats are 'standard' and 'json'.
|
|
LogFormat LogFormat
|
|
|
|
// LogFilePath is the path to write the logs to the user specified file.
|
|
LogFilePath string
|
|
|
|
// LogRotateDuration is the user specified time to rotate logs
|
|
LogRotateDuration time.Duration
|
|
|
|
// LogRotateBytes is the user specified byte limit to rotate logs
|
|
LogRotateBytes int
|
|
|
|
// LogRotateMaxFiles is the maximum number of past archived log files to keep
|
|
LogRotateMaxFiles int
|
|
|
|
// DefaultFileName should be set to the value to be used if the LogFilePath
|
|
// ends in a path separator such as '/var/log/'
|
|
// Examples of the default name are as follows: 'vault', 'agent' or 'proxy.
|
|
// The creator of this struct *must* ensure that it is assigned before doing
|
|
// anything with LogConfig!
|
|
DefaultFileName string
|
|
}
|
|
|
|
// NewLogConfig should be used to initialize the LogConfig struct.
|
|
func NewLogConfig(defaultFileName string) (*LogConfig, error) {
|
|
defaultFileName = strings.TrimSpace(defaultFileName)
|
|
if defaultFileName == "" {
|
|
return nil, errors.New("default file name is required")
|
|
}
|
|
|
|
return &LogConfig{DefaultFileName: defaultFileName}, nil
|
|
}
|
|
|
|
func (c *LogConfig) isLevelInvalid() bool {
|
|
return c.LogLevel == hclog.NoLevel || c.LogLevel == hclog.Off || c.LogLevel.String() == "unknown"
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// parseFullPath takes a full path intended to be the location for log files and
|
|
// breaks it down into a directory and a file name. It checks both of these for
|
|
// the common globbing character '*' and returns an error if it is present.
|
|
func parseFullPath(fullPath string) (directory, fileName string, err error) {
|
|
directory, fileName = filepath.Split(fullPath)
|
|
|
|
globChars := "*?["
|
|
if strings.ContainsAny(directory, globChars) {
|
|
err = multierror.Append(err, fmt.Errorf("directory contains glob character"))
|
|
}
|
|
if fileName == "" {
|
|
fileName = "vault.log"
|
|
} else if strings.ContainsAny(fileName, globChars) {
|
|
err = multierror.Append(err, fmt.Errorf("file name contains globbing character"))
|
|
}
|
|
|
|
return directory, fileName, err
|
|
}
|
|
|
|
// Setup creates a new logger with the specified configuration and writer
|
|
func Setup(config *LogConfig, w io.Writer) (hclog.InterceptLogger, error) {
|
|
// Validate the log level
|
|
if config.isLevelInvalid() {
|
|
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}}
|
|
|
|
// Create a file logger if the user has specified the path to the log file
|
|
if config.LogFilePath != "" {
|
|
dir, fileName, err := parseFullPath(config.LogFilePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fileName == "" {
|
|
fileName = fmt.Sprintf("%s.log", config.DefaultFileName)
|
|
}
|
|
if config.LogRotateDuration == 0 {
|
|
config.LogRotateDuration = defaultRotateDuration
|
|
}
|
|
|
|
logFile := &LogFile{
|
|
fileName: fileName,
|
|
logPath: dir,
|
|
duration: config.LogRotateDuration,
|
|
maxBytes: config.LogRotateBytes,
|
|
maxArchivedFiles: config.LogRotateMaxFiles,
|
|
}
|
|
if err := logFile.pruneFiles(); err != nil {
|
|
return nil, fmt.Errorf("failed to prune log files: %w", err)
|
|
}
|
|
if err := logFile.openNew(); err != nil {
|
|
return nil, fmt.Errorf("failed to setup logging: %w", err)
|
|
}
|
|
writers = append(writers, logFile)
|
|
}
|
|
|
|
logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{
|
|
Name: config.Name,
|
|
Level: config.LogLevel,
|
|
IndependentLevels: true,
|
|
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) (hclog.Level, error) {
|
|
var result hclog.Level
|
|
logLevel = strings.ToLower(strings.TrimSpace(logLevel))
|
|
|
|
switch logLevel {
|
|
case "trace":
|
|
result = hclog.Trace
|
|
case "debug":
|
|
result = hclog.Debug
|
|
case "notice", "info", "":
|
|
result = hclog.Info
|
|
case "warn", "warning":
|
|
result = hclog.Warn
|
|
case "err", "error":
|
|
result = hclog.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 hclog.Logger) (string, error) {
|
|
logLevel := logger.GetLevel()
|
|
|
|
switch logLevel {
|
|
case hclog.Trace, hclog.Debug, hclog.Info, hclog.Warn, hclog.Error:
|
|
return logLevel.String(), nil
|
|
default:
|
|
return "", fmt.Errorf("unknown log level")
|
|
}
|
|
}
|