vault/helper/logging/logger.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

217 lines
6.0 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package logging
import (
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"time"
log "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 log.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
// SubloggerHook handles creation of new subloggers, automatically appending
// them to core's running list of allLoggers.
// see: server.AppendToAllLoggers for more details.
SubloggerHook func(log.Logger) log.Logger
}
// SubloggerAdder is an interface which facilitates tracking of new subloggers
// added between phases of server startup.
type SubloggerAdder interface {
SubloggerHook(logger log.Logger) log.Logger
}
func (c *LogConfig) isLevelInvalid() bool {
return c.LogLevel == log.NoLevel || c.LogLevel == log.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) (log.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 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 := log.NewInterceptLogger(&log.LoggerOptions{
Name: config.Name,
Level: config.LogLevel,
IndependentLevels: true,
Output: io.MultiWriter(writers...),
JSONFormat: config.isFormatJson(),
SubloggerHook: config.SubloggerHook,
})
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) {
logLevel := logger.GetLevel()
switch logLevel {
case log.Trace, log.Debug, log.Info, log.Warn, log.Error:
return logLevel.String(), nil
default:
return "", fmt.Errorf("unknown log level")
}
}