// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package logging import ( "fmt" "io" "path/filepath" "time" "github.com/hashicorp/go-hclog" gsyslog "github.com/hashicorp/go-syslog" ) // Config is used to set up logging. type Config struct { // LogLevel is the minimum level to be logged. LogLevel string // LogJSON controls outputing logs in a JSON format. LogJSON bool // Name is the name the returned logger will use to prefix log lines. Name string // EnableSyslog controls forwarding to syslog. EnableSyslog bool // SyslogFacility is the destination for syslog forwarding. SyslogFacility string // 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 } // defaultRotateDuration is the default time taken by the agent to rotate logs const defaultRotateDuration = 24 * time.Hour type LogSetupErrorFn func(string) // 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 logging from Config, and return an hclog Logger. // // Logs may be written to out, and optionally to syslog, and a file. func Setup(config Config, out io.Writer) (hclog.InterceptLogger, error) { if !ValidateLogLevel(config.LogLevel) { return nil, fmt.Errorf("Invalid log level: %s. Valid log levels are: %v", config.LogLevel, allowedLogLevels) } // If out is os.Stdout and Consul 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: out}} if config.EnableSyslog { retries := 12 delay := 5 * time.Second for i := 0; i <= retries; i++ { syslog, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, config.SyslogFacility, "consul") if err == nil { writers = append(writers, &SyslogWrapper{l: syslog}) break } if i == retries { timeout := time.Duration(retries) * delay return nil, fmt.Errorf("Syslog setup did not succeed within timeout (%s).", timeout.String()) } time.Sleep(delay) } } // Create a file logger if the user has specified the path to the log file if config.LogFilePath != "" { dir, fileName := filepath.Split(config.LogFilePath) if fileName == "" { fileName = "consul.log" } if config.LogRotateDuration == 0 { config.LogRotateDuration = defaultRotateDuration } logFile := &LogFile{ fileName: fileName, logPath: dir, duration: config.LogRotateDuration, MaxBytes: config.LogRotateBytes, MaxFiles: 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{ Level: LevelFromString(config.LogLevel), Name: config.Name, Output: io.MultiWriter(writers...), JSONFormat: config.LogJSON, }) return logger, nil }