diff --git a/README.md b/README.md index fe6e3aae6..5f6f73fd2 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ this functionality might prove useful. - [Math Functions](docs/templating-language.md#math-functions) - [Observability](docs/observability.md) - [Logging](docs/observability.md#logging) + - [Logging to file](docs/observability.md#logging-to-file) - [Modes](docs/modes.md) - [Once Mode](docs/modes.md#once-mode) - [De-Duplication Mode](docs/modes.md#de-duplication-mode) @@ -44,7 +45,7 @@ this functionality might prove useful. - [Plugins](docs/plugins.md) - [Caveats](#caveats) - [Docker Image Use](#docker-image-use) -- [Dots in Service Names](#dots-in-service-names) +- [Dots in Service Names](#dots-in-service-names) - [Termination on Error](#termination-on-error) - [Commands](#commands) - [Environment](#environment) @@ -235,7 +236,7 @@ users the ability to further customize their command script. #### Multiple Commands The command configured for running on template rendering must take one of two -forms. +forms. The first is as a single command without spaces in its name and no arguments. This form of command will be called directly by consul-template and is good for diff --git a/cli.go b/cli.go index f55e2f163..ada9df3df 100644 --- a/cli.go +++ b/cli.go @@ -387,6 +387,26 @@ func (cli *CLI) ParseFlags(args []string) ( return nil }), "log-level", "") + flags.Var((funcVar)(func(s string) error { + c.FileLog.LogFilePath = config.String(s) + return nil + }), "log-file", "") + + flags.Var((funcIntVar)(func(i int) error { + c.FileLog.LogRotateBytes = config.Int(i) + return nil + }), "log-rotate-bytes", "") + + flags.Var((funcDurationVar)(func(d time.Duration) error { + c.FileLog.LogRotateDuration = config.TimeDuration(d) + return nil + }), "log-rotate-duration", "") + + flags.Var((funcIntVar)(func(i int) error { + c.FileLog.LogRotateMaxFiles = config.Int(i) + return nil + }), "log-rotate-max-files", "") + flags.Var((funcDurationVar)(func(d time.Duration) error { c.MaxStale = config.TimeDuration(d) return nil @@ -605,11 +625,15 @@ func logError(err error, status int) int { func (cli *CLI) setup(conf *config.Config) (*config.Config, error) { if err := logging.Setup(&logging.Config{ - Level: config.StringVal(conf.LogLevel), - Syslog: config.BoolVal(conf.Syslog.Enabled), - SyslogFacility: config.StringVal(conf.Syslog.Facility), - SyslogName: config.StringVal(conf.Syslog.Name), - Writer: cli.errStream, + Level: config.StringVal(conf.LogLevel), + LogFilePath: config.StringVal(conf.FileLog.LogFilePath), + LogRotateBytes: config.IntVal(conf.FileLog.LogRotateBytes), + LogRotateDuration: config.TimeDurationVal(conf.FileLog.LogRotateDuration), + LogRotateMaxFiles: config.IntVal(conf.FileLog.LogRotateMaxFiles), + Syslog: config.BoolVal(conf.Syslog.Enabled), + SyslogFacility: config.StringVal(conf.Syslog.Facility), + SyslogName: config.StringVal(conf.Syslog.Name), + Writer: cli.errStream, }); err != nil { return nil, err } diff --git a/cli_test.go b/cli_test.go index 659f569da..1f8e5c760 100644 --- a/cli_test.go +++ b/cli_test.go @@ -355,6 +355,46 @@ func TestCLI_ParseFlags(t *testing.T) { }, false, }, + { + "log-file", + []string{"-log-file", "something.log"}, + &config.Config{ + FileLog: &config.LogFileConfig{ + LogFilePath: config.String("something.log"), + }, + }, + false, + }, + { + "log-rotate-bytes", + []string{"-log-rotate-bytes", "102400"}, + &config.Config{ + FileLog: &config.LogFileConfig{ + LogRotateBytes: config.Int(102400), + }, + }, + false, + }, + { + "log-rotate-duration", + []string{"-log-rotate-duration", "24h"}, + &config.Config{ + FileLog: &config.LogFileConfig{ + LogRotateDuration: config.TimeDuration(24 * time.Hour), + }, + }, + false, + }, + { + "log-rotate-max-files", + []string{"-log-rotate-max-files", "10"}, + &config.Config{ + FileLog: &config.LogFileConfig{ + LogRotateMaxFiles: config.Int(10), + }, + }, + false, + }, { "max-stale", []string{"-max-stale", "10s"}, diff --git a/config/config.go b/config/config.go index 5d72080f8..2d52a0425 100644 --- a/config/config.go +++ b/config/config.go @@ -63,6 +63,9 @@ type Config struct { // LogLevel is the level with which to log for this config. LogLevel *string `mapstructure:"log_level"` + // FileLog is the configuration for file logging. + FileLog *LogFileConfig `mapstructure:"log_file"` + // MaxStale is the maximum amount of time for staleness from Consul as given // by LastContact. If supplied, Consul Template will query all servers instead // of just the leader. @@ -131,6 +134,10 @@ func (c *Config) Copy() *Config { o.ReloadSignal = c.ReloadSignal + if c.FileLog != nil { + o.FileLog = c.FileLog.Copy() + } + if c.Syslog != nil { o.Syslog = c.Syslog.Copy() } @@ -206,6 +213,10 @@ func (c *Config) Merge(o *Config) *Config { r.ReloadSignal = o.ReloadSignal } + if o.FileLog != nil { + r.FileLog = r.FileLog.Merge(o.FileLog) + } + if o.Syslog != nil { r.Syslog = r.Syslog.Merge(o.Syslog) } @@ -256,6 +267,7 @@ func Parse(s string) (*Config, error) { "env", "exec", "exec.env", + "log_file", "ssl", "syslog", "vault", @@ -414,6 +426,7 @@ func (c *Config) GoString() string { "MaxStale:%s, "+ "PidFile:%s, "+ "ReloadSignal:%s, "+ + "FileLog:%#v, "+ "Syslog:%#v, "+ "Templates:%#v, "+ "Vault:%#v, "+ @@ -430,6 +443,7 @@ func (c *Config) GoString() string { TimeDurationGoString(c.MaxStale), StringGoString(c.PidFile), SignalGoString(c.ReloadSignal), + c.FileLog, c.Syslog, c.Templates, c.Vault, @@ -474,6 +488,7 @@ func DefaultConfig() *Config { Dedup: DefaultDedupConfig(), DefaultDelims: DefaultDefaultDelims(), Exec: DefaultExecConfig(), + FileLog: DefaultLogFileConfig(), Syslog: DefaultSyslogConfig(), Templates: DefaultTemplateConfigs(), Vault: DefaultVaultConfig(), @@ -533,6 +548,11 @@ func (c *Config) Finalize() { c.ReloadSignal = Signal(DefaultReloadSignal) } + if c.FileLog == nil { + c.FileLog = DefaultLogFileConfig() + } + c.FileLog.Finalize() + if c.Syslog == nil { c.Syslog = DefaultSyslogConfig() } diff --git a/config/config_test.go b/config/config_test.go index 68a6a1c13..80e5672fa 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -538,6 +538,74 @@ func TestParse(t *testing.T) { }, false, }, + { + "log_file", + `log_file {}`, + &Config{ + FileLog: &LogFileConfig{}, + }, + false, + }, + { + "log_file_path", + `log_file { + path = "something.log" + }`, + &Config{ + FileLog: &LogFileConfig{ + LogFilePath: String("something.log"), + }, + }, + false, + }, + { + "log_file_path_no_filename", + `log_file { + path = "./logs" + }`, + &Config{ + FileLog: &LogFileConfig{ + LogFilePath: String("./logs"), + }, + }, + false, + }, + { + "log_file_log_rotate_bytes", + `log_file { + log_rotate_bytes = 102400 + }`, + &Config{ + FileLog: &LogFileConfig{ + LogRotateBytes: Int(102400), + }, + }, + false, + }, + { + "log_file_log_rotate_duration", + `log_file { + log_rotate_duration = "24h" + }`, + &Config{ + FileLog: &LogFileConfig{ + LogRotateDuration: TimeDuration(24 * time.Hour), + }, + }, + false, + }, + { + "log_file_log_rotate_max_files", + `log_file { + log_rotate_max_files = 10 + }`, + &Config{ + FileLog: &LogFileConfig{ + LogRotateMaxFiles: Int(10), + }, + }, + false, + }, { "max_stale", `max_stale = "10s"`, @@ -1783,6 +1851,24 @@ func TestConfig_Merge(t *testing.T) { LogLevel: String("log_level-diff"), }, }, + { + "file_log", + &Config{ + FileLog: &LogFileConfig{ + LogFilePath: String("something.log"), + }, + }, + &Config{ + FileLog: &LogFileConfig{ + LogFilePath: String("somethingelse.log"), + }, + }, + &Config{ + FileLog: &LogFileConfig{ + LogFilePath: String("somethingelse.log"), + }, + }, + }, { "max_stale", &Config{ diff --git a/config/logfile.go b/config/logfile.go new file mode 100644 index 000000000..1dc15c327 --- /dev/null +++ b/config/logfile.go @@ -0,0 +1,129 @@ +package config + +import ( + "fmt" + "time" + + "github.com/hashicorp/consul-template/version" +) + +var ( + // DefaultLogFileName is the default filename if the user didn't specify one + // which means that the user specified a directory to log to + DefaultLogFileName = fmt.Sprintf("%s.log", version.Name) + + // DefaultLogRotateDuration is the default time taken by the agent to rotate logs + DefaultLogRotateDuration = 24 * time.Hour +) + +type LogFileConfig struct { + // LogFilePath is the path to the file the logs get written to + LogFilePath *string `mapstructure:"path"` + + // LogRotateBytes is the maximum number of bytes that should be written to a log + // file + LogRotateBytes *int `mapstructure:"log_rotate_bytes"` + + // LogRotateDuration is the time after which log rotation needs to be performed + LogRotateDuration *time.Duration `mapstructure:"log_rotate_duration"` + + // LogRotateMaxFiles is the maximum number of log file archives to keep + LogRotateMaxFiles *int `mapstructure:"log_rotate_max_files"` +} + +// DefaultLogFileConfig returns a configuration that is populated with the +// default values. +func DefaultLogFileConfig() *LogFileConfig { + return &LogFileConfig{} +} + +// Copy returns a deep copy of this configuration. +func (c *LogFileConfig) Copy() *LogFileConfig { + if c == nil { + return nil + } + + var o LogFileConfig + o.LogFilePath = c.LogFilePath + o.LogRotateBytes = c.LogRotateBytes + o.LogRotateDuration = c.LogRotateDuration + o.LogRotateMaxFiles = c.LogRotateMaxFiles + return &o +} + +// Merge combines all values in this configuration with the values in the other +// configuration, with values in the other configuration taking precedence. +// Maps and slices are merged, most other values are overwritten. Complex +// structs define their own merge functionality. +func (c *LogFileConfig) Merge(o *LogFileConfig) *LogFileConfig { + if c == nil { + if o == nil { + return nil + } + return o.Copy() + } + + if o == nil { + return c.Copy() + } + + r := c.Copy() + + if o.LogFilePath != nil { + r.LogFilePath = o.LogFilePath + } + + if o.LogRotateBytes != nil { + r.LogRotateBytes = o.LogRotateBytes + } + + if o.LogRotateDuration != nil { + r.LogRotateDuration = o.LogRotateDuration + } + + if o.LogRotateMaxFiles != nil { + r.LogRotateMaxFiles = o.LogRotateMaxFiles + } + + return r +} + +// Finalize ensures there no nil pointers. +func (c *LogFileConfig) Finalize() { + + if c.LogFilePath == nil { + c.LogFilePath = String("") + } + + if c.LogRotateBytes == nil { + c.LogRotateBytes = Int(0) + } + + if c.LogRotateDuration == nil { + c.LogRotateDuration = TimeDuration(DefaultLogRotateDuration) + } + + if c.LogRotateMaxFiles == nil { + c.LogRotateMaxFiles = Int(0) + } + +} + +// GoString defines the printable version of this struct. +func (c *LogFileConfig) GoString() string { + if c == nil { + return "(*LogFileConfig)(nil)" + } + + return fmt.Sprintf("&LogFileConfig{"+ + "LogFilePath:%s, "+ + "LogRotateBytes:%s, "+ + "LogRotateDuration:%s, "+ + "LogRotateMaxFiles:%s, "+ + "}", + StringGoString(c.LogFilePath), + IntGoString(c.LogRotateBytes), + TimeDurationGoString(c.LogRotateDuration), + IntGoString(c.LogRotateMaxFiles), + ) +} diff --git a/docs/configuration.md b/docs/configuration.md index 6c0dfe4ab..77a37efde 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -194,6 +194,31 @@ syslog { # This is the name of the syslog facility to log to. facility = "LOCAL5" } + +# This block defines the configuration for logging to file +log_file { + # If a path is specified, the feature is enabled + # Please refer to the documentation for the -log-file + # CLI flag for more information about its behaviour + path = "/var/log/something.log" + + # This allow you to control the number of bytes that + # should be written to a log before it needs to be + # rotated. Unless specified, there is no limit to the + # number of bytes that can be written to a log file + log_rotate_bytes = 1024000 + + # This lets you control time based rotation, by default + # logs are rotated every 24h + log_rotate_duration = "3h" + + # This lets you control the maximum number of older log + # file archives to keep. Defaults to 0 (no files are ever + # deleted). + # Set to -1 to discard old log files when a new one is + # created + log_rotate_max_files = 10 +} ``` ## Consul @@ -347,7 +372,7 @@ vault { # documentation for more information. unwrap_token = true - # The default lease duration Consul Template will use on a Vault secret that + # The default lease duration Consul Template will use on a Vault secret that # does not have a lease duration. This is used to calculate the sleep duration # for rechecking a Vault secret value. This field is optional and will default to # 5 minutes. diff --git a/docs/observability.md b/docs/observability.md index 5d2e90585..6d19dbc06 100644 --- a/docs/observability.md +++ b/docs/observability.md @@ -41,3 +41,31 @@ $ consul-template -log-level debug ... # ... ``` +## Logging to file + +Consul Template can log to file as well. +Particularly useful in use cases where it's not trivial to capture *stdout* and/or *stderr* +like for example when Consul Template is deployed as a Windows Service. + +These are the relevant CLI flags: + +- `-log-file` - writes all the Consul Template log messages + to a file. This value is used as a prefix for the log file name. The current timestamp + is appended to the file name. If the value ends in a path separator, `consul-template-` + will be appended to the value. If the file name is missing an extension, `.log` + is appended. For example, setting `log-file` to `/var/log/` would result in a log + file path of `/var/log/consul-template-{timestamp}.log`. `log-file` can be combined with + `-log-rotate-bytes` and `-log-rotate-duration` + for a fine-grained log rotation experience. + +- `-log-rotate-bytes` - to specify the number of + bytes that should be written to a log before it needs to be rotated. Unless specified, + there is no limit to the number of bytes that can be written to a log file. + +- `-log-rotate-duration` - to specify the maximum + duration a log should be written to before it needs to be rotated. Must be a duration + value such as 30s. Defaults to 24h. + +- `-log-rotate-max-files` - to specify the maximum + number of older log file archives to keep. Defaults to 0 (no files are ever deleted). + Set to -1 to discard old log files when a new one is created. \ No newline at end of file diff --git a/logging/logfile.go b/logging/logfile.go new file mode 100644 index 000000000..5834284fd --- /dev/null +++ b/logging/logfile.go @@ -0,0 +1,147 @@ +package logging + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/hashicorp/logutils" +) + +type LogFile struct { + //Name of the log file + fileName string + + //Path to the log file + logPath string + + //Duration between each file rotation operation + duration time.Duration + + //LastCreated represents the creation time of the latest log + LastCreated time.Time + + //FileInfo is the pointer to the current file being written to + FileInfo *os.File + + //MaxBytes is the maximum number of desired bytes for a log file + MaxBytes int + + //BytesWritten is the number of bytes written in the current log file + BytesWritten int64 + + // Max rotated files to keep before removing them. + MaxFiles int + + //filt is used to filter log messages depending on their level + filt *logutils.LevelFilter + + //acquire is the mutex utilized to ensure we have no concurrency issues + acquire sync.Mutex +} + +func (l *LogFile) fileNamePattern() string { + // Extract the file extension + fileExt := filepath.Ext(l.fileName) + // If we have no file extension we append .log + if fileExt == "" { + fileExt = ".log" + } + // Remove the file extension from the filename + return strings.TrimSuffix(l.fileName, fileExt) + "-%s" + fileExt +} + +func (l *LogFile) openNew() error { + fileNamePattern := l.fileNamePattern() + + createTime := time.Now() + newfileName := fmt.Sprintf(fileNamePattern, strconv.FormatInt(createTime.UnixNano(), 10)) + newfilePath := filepath.Join(l.logPath, newfileName) + + // Try creating a file. We truncate the file because we are the only authority to write the logs + filePointer, err := os.OpenFile(newfilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0640) + if err != nil { + return err + } + + l.FileInfo = filePointer + // New file, new bytes tracker, new creation time :) + l.LastCreated = createTime + l.BytesWritten = 0 + return nil +} + +func (l *LogFile) rotate() error { + // Get the time from the last point of contact + timeElapsed := time.Since(l.LastCreated) + // Rotate if we hit the byte file limit or the time limit + if (l.BytesWritten >= int64(l.MaxBytes) && (l.MaxBytes > 0)) || timeElapsed >= l.duration { + l.FileInfo.Close() + if err := l.pruneFiles(); err != nil { + return err + } + return l.openNew() + } + return nil +} + +func (l *LogFile) pruneFiles() error { + if l.MaxFiles == 0 { + return nil + } + + pattern := filepath.Join(l.logPath, fmt.Sprintf(l.fileNamePattern(), "*")) + matches, err := filepath.Glob(pattern) + if err != nil { + return err + } + + switch { + case l.MaxFiles < 0: + return removeFiles(matches) + case len(matches) < l.MaxFiles: + return nil + } + + sort.Strings(matches) + last := len(matches) - l.MaxFiles + return removeFiles(matches[:last]) +} + +func removeFiles(files []string) error { + for _, file := range files { + if err := os.Remove(file); err != nil { + return err + } + } + return nil +} + +// Write is used to implement io.Writer. +func (l *LogFile) Write(b []byte) (int, error) { + l.acquire.Lock() + defer l.acquire.Unlock() + + // Skip if the log level doesn't apply + if l.filt != nil && !l.filt.Check(b) { + return 0, nil + } + + // 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 + } + } + // Check for the last contact and rotate if necessary + if err := l.rotate(); err != nil { + return 0, err + } + l.BytesWritten += int64(len(b)) + return l.FileInfo.Write(b) +} diff --git a/logging/logfile_test.go b/logging/logfile_test.go new file mode 100644 index 000000000..194e763a8 --- /dev/null +++ b/logging/logfile_test.go @@ -0,0 +1,206 @@ +package logging + +import ( + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" + "time" + + "github.com/hashicorp/consul-template/config" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/hashicorp/logutils" + "github.com/stretchr/testify/require" +) + +func TestLogFileFilter(t *testing.T) { + + filt, err := newLogFilter(ioutil.Discard, logutils.LogLevel("INFO")) + if err != nil { + t.Fatal(err) + } + + tempDir := testutil.TempDir(t, "") + logFile := LogFile{ + fileName: "something.log", + logPath: tempDir, + duration: 50 * time.Millisecond, + filt: filt, + } + + logFile.Write([]byte("Hello World")) + time.Sleep(3 * logFile.duration) + logFile.Write([]byte("Second File")) + require.Len(t, listDir(t, tempDir), 2) + + infotest := []byte("[INFO] test") + n, err := logFile.Write(infotest) + if err != nil { + t.Fatalf("err: %s", err) + } + if n == 0 { + t.Fatalf("should have logged") + } + if n != len(infotest) { + t.Fatalf("byte count (%d) doesn't match output len (%d).", + n, len(infotest)) + } + + n, err = logFile.Write([]byte("[DEBUG] test")) + if err != nil { + t.Fatalf("err: %s", err) + } + if n != 0 { + t.Fatalf("should not have logged") + } +} + +func TestLogFileNoFilter(t *testing.T) { + + tempDir := testutil.TempDir(t, "") + logFile := LogFile{ + fileName: "something.log", + logPath: tempDir, + duration: 50 * time.Millisecond, + } + + logFile.Write([]byte("Hello World")) + time.Sleep(3 * logFile.duration) + logFile.Write([]byte("Second File")) + require.Len(t, listDir(t, tempDir), 2) + + infotest := []byte("[INFO] test") + n, err := logFile.Write(infotest) + if err != nil { + t.Fatalf("err: %s", err) + } + if n == 0 { + t.Fatalf("should have logged") + } + if n != len(infotest) { + t.Fatalf("byte count (%d) doesn't match output len (%d).", + n, len(infotest)) + } + + n, err = logFile.Write([]byte("[DEBUG] test")) + if err != nil { + t.Fatalf("err: %s", err) + } + if n == 0 { + t.Fatalf("should have logged") + } +} + +func TestLogFile_Rotation_MaxDuration(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + tempDir := testutil.TempDir(t, "") + logFile := LogFile{ + fileName: "something.log", + logPath: tempDir, + duration: 50 * time.Millisecond, + } + + logFile.Write([]byte("Hello World")) + time.Sleep(3 * logFile.duration) + logFile.Write([]byte("Second File")) + require.Len(t, listDir(t, tempDir), 2) +} + +func TestLogFile_openNew(t *testing.T) { + logFile := LogFile{ + fileName: "something.log", + logPath: testutil.TempDir(t, ""), + duration: config.DefaultLogRotateDuration, + } + err := logFile.openNew() + require.NoError(t, err) + + msg := "[INFO] Something" + _, err = logFile.Write([]byte(msg)) + require.NoError(t, err) + + content, err := ioutil.ReadFile(logFile.FileInfo.Name()) + require.NoError(t, err) + require.Contains(t, string(content), msg) +} + +func TestLogFile_Rotation_MaxBytes(t *testing.T) { + tempDir := testutil.TempDir(t, "LogWriterBytes") + logFile := LogFile{ + fileName: "somefile.log", + logPath: tempDir, + MaxBytes: 10, + duration: config.DefaultLogRotateDuration, + } + logFile.Write([]byte("Hello World")) + logFile.Write([]byte("Second File")) + require.Len(t, listDir(t, tempDir), 2) +} + +func TestLogFile_PruneFiles(t *testing.T) { + tempDir := testutil.TempDir(t, t.Name()) + logFile := LogFile{ + fileName: "something.log", + logPath: tempDir, + MaxBytes: 10, + duration: config.DefaultLogRotateDuration, + MaxFiles: 1, + } + logFile.Write([]byte("[INFO] Hello World")) + logFile.Write([]byte("[INFO] Second File")) + logFile.Write([]byte("[INFO] Third File")) + + logFiles := listDir(t, tempDir) + sort.Strings(logFiles) + require.Len(t, logFiles, 2) + + content, err := ioutil.ReadFile(filepath.Join(tempDir, logFiles[0])) + require.NoError(t, err) + require.Contains(t, string(content), "Second File") + + content, err = ioutil.ReadFile(filepath.Join(tempDir, logFiles[1])) + require.NoError(t, err) + require.Contains(t, string(content), "Third File") +} + +func TestLogFile_PruneFiles_Disabled(t *testing.T) { + tempDir := testutil.TempDir(t, t.Name()) + logFile := LogFile{ + fileName: "somename.log", + logPath: tempDir, + MaxBytes: 10, + duration: config.DefaultLogRotateDuration, + MaxFiles: 0, + } + logFile.Write([]byte("[INFO] Hello World")) + logFile.Write([]byte("[INFO] Second File")) + logFile.Write([]byte("[INFO] Third File")) + require.Len(t, listDir(t, tempDir), 3) +} + +func TestLogFile_FileRotation_Disabled(t *testing.T) { + tempDir := testutil.TempDir(t, t.Name()) + logFile := LogFile{ + fileName: "something.log", + logPath: tempDir, + MaxBytes: 10, + MaxFiles: -1, + } + logFile.Write([]byte("[INFO] Hello World")) + logFile.Write([]byte("[INFO] Second File")) + logFile.Write([]byte("[INFO] Third File")) + require.Len(t, listDir(t, tempDir), 1) +} + +func listDir(t *testing.T, name string) []string { + t.Helper() + fh, err := os.Open(name) + require.NoError(t, err) + files, err := fh.Readdirnames(100) + require.NoError(t, err) + return files +} diff --git a/logging/logging.go b/logging/logging.go index 4d65d42f8..dcde4da56 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -5,9 +5,12 @@ import ( "io" "io/ioutil" "log" + "path/filepath" "strings" "time" + cnf "github.com/hashicorp/consul-template/config" + gsyslog "github.com/hashicorp/go-syslog" "github.com/hashicorp/logutils" ) @@ -43,6 +46,19 @@ type Config struct { // Level is the log level to use. Level string `json:"level"` + // LogFilePath is the path to the file the logs get written to + LogFilePath string `json:"log_file"` + + // LogRotateBytes is the maximum number of bytes that should be written to a log + // file + LogRotateBytes int `json:"log_rotate_bytes"` + + // LogRotateDuration is the time after which log rotation needs to be performed + LogRotateDuration time.Duration `json:"log_rotate_duration"` + + // LogRotateMaxFiles is the maximum number of log file archives to keep + LogRotateMaxFiles int `json:"log_rotate_max_files"` + // Syslog and SyslogFacility are the syslog configuration options. Syslog bool `json:"syslog"` SyslogFacility string `json:"syslog_facility"` @@ -76,6 +92,34 @@ func newWriter(config *Config) (io.Writer, error) { return nil, err } + if config.LogFilePath != "" { + dir, fileName := filepath.Split(config.LogFilePath) + if fileName == "" { + fileName = cnf.DefaultLogFileName + } + if config.LogRotateDuration == 0 { + config.LogRotateDuration = cnf.DefaultLogRotateDuration + } + log.Printf("[DEBUG] (logging) enabling log_file logging to %s with rotation every %s", + filepath.Join(dir, fileName), config.LogRotateDuration, + ) + logFile := &LogFile{ + filt: logOutput.(*logutils.LevelFilter), + fileName: fileName, + logPath: dir, + duration: config.LogRotateDuration, + MaxBytes: config.LogRotateBytes, + MaxFiles: config.LogRotateMaxFiles, + } + if err := logFile.pruneFiles(); err != nil { + return nil, fmt.Errorf("error while pruning log files: %w", err) + } + if err := logFile.openNew(); err != nil { + return nil, fmt.Errorf("error setting up log_file logging : %w", err) + } + logOutput = io.MultiWriter(logOutput, logFile) + } + if config.Syslog { log.Printf("[DEBUG] (logging) enabling syslog on %s", config.SyslogFacility)