diff --git a/docs/pages/reference/signals.mdx b/docs/pages/reference/signals.mdx index 95139796f935..31d30ce0795f 100644 --- a/docs/pages/reference/signals.mdx +++ b/docs/pages/reference/signals.mdx @@ -20,3 +20,4 @@ $ kill -SIG | `TERM`, `INT` | Immediate non-graceful shutdown. All existing connections will be closed after a very short delay. | | `USR2` | Forks a new Teleport daemon to serve new connections. | | `HUP` | Forks a new Teleport daemon to serve new connections **and** initiates the graceful shutdown of the existing process, same as `SIGQUIT`. | +| `CONT` | Re-open the log file (useful for `logrotate`). | diff --git a/lib/config/configuration.go b/lib/config/configuration.go index 589113a4476c..7eb46a90fd83 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -26,6 +26,7 @@ import ( "crypto/x509" "errors" "io" + "io/fs" "log/slog" "maps" "net" @@ -756,12 +757,12 @@ func applyLogConfig(loggerConfig Log, cfg *servicecfg.Config) error { var w io.Writer switch loggerConfig.Output { case "": - w = os.Stderr + w = logutils.NewSharedWriter(os.Stderr) case "stderr", "error", "2": - w = os.Stderr + w = logutils.NewSharedWriter(os.Stderr) cfg.Console = io.Discard // disable console printing case "stdout", "out", "1": - w = os.Stdout + w = logutils.NewSharedWriter(os.Stdout) cfg.Console = io.Discard // disable console printing case teleport.Syslog: w = os.Stderr @@ -779,14 +780,22 @@ func applyLogConfig(loggerConfig Log, cfg *servicecfg.Config) error { logger.ReplaceHooks(make(log.LevelHooks)) logger.AddHook(hook) + // If syslog output has been configured and is supported by the operating system, + // then the shared writer is not needed because the syslog writer is already + // protected with a mutex. w = sw default: // assume it's a file path: - logFile, err := os.Create(loggerConfig.Output) + var flag int = os.O_WRONLY | os.O_CREATE | os.O_APPEND + var mode = fs.FileMode(0666) + logFile, err := os.OpenFile(loggerConfig.Output, flag, mode) if err != nil { return trace.Wrap(err, "failed to create the log file") } - w = logFile + fileWriter := logutils.NewFileSharedWriter(logFile, flag, mode) + cfg.LogFileReopen = fileWriter.Reopen + + w = fileWriter } level := new(slog.LevelVar) @@ -815,12 +824,6 @@ func applyLogConfig(loggerConfig Log, cfg *servicecfg.Config) error { return trace.Wrap(err) } - // If syslog output has been configured and is supported by the operating system, - // then the shared writer is not needed because the syslog writer is already - // protected with a mutex. - if len(logger.Hooks) == 0 { - w = logutils.NewSharedWriter(w) - } var slogLogger *slog.Logger switch strings.ToLower(loggerConfig.Format.Output) { case "": diff --git a/lib/service/servicecfg/config.go b/lib/service/servicecfg/config.go index 456321cdf35e..7d547d60df51 100644 --- a/lib/service/servicecfg/config.go +++ b/lib/service/servicecfg/config.go @@ -227,6 +227,9 @@ type Config struct { Logger *slog.Logger // LoggerLevel defines the Logger log level. LoggerLevel *slog.LevelVar + // LogFileReopen is used to close and re-open the log file. + // If the logger is not writting to a log file, this is a no-op. + LogFileReopen func() error // PluginRegistry allows adding enterprise logic to Teleport services PluginRegistry plugin.Registry @@ -592,6 +595,8 @@ func ApplyDefaults(cfg *Config) { cfg.MaxRetryPeriod = defaults.MaxWatcherBackoff cfg.Testing.ConnectFailureC = make(chan time.Duration, 1) cfg.CircuitBreakerConfig = breaker.DefaultBreakerConfig(cfg.Clock) + + cfg.LogFileReopen = func() error { return nil } } // FileDescriptor is a file descriptor associated diff --git a/lib/service/signals.go b/lib/service/signals.go index ebcdadfac501..3eb8af9a580e 100644 --- a/lib/service/signals.go +++ b/lib/service/signals.go @@ -76,6 +76,7 @@ var teleportSignals = []os.Signal{ syscall.SIGUSR1, // log process diagnostic info syscall.SIGUSR2, // initiate process restart procedure syscall.SIGHUP, // graceful restart procedure + syscall.SIGCONT, // Re-open the log file } // WaitForSignals waits for system signals and processes them. @@ -151,6 +152,11 @@ func (process *TeleportProcess) WaitForSignals(ctx context.Context, sigC <-chan process.Shutdown(timeoutCtx) process.logger.InfoContext(process.ExitContext(), "All services stopped, exiting.") return nil + case syscall.SIGCONT: + process.logger.InfoContext(process.ExitContext(), "Rotating log file.") + if err := process.Config.LogFileReopen(); err != nil { + return trace.Wrap(err) + } default: process.logger.InfoContext(process.ExitContext(), "Ignoring unknown signal.", "signal", signal) } diff --git a/lib/utils/log/writer.go b/lib/utils/log/writer.go index 77cf3037a8b6..510b954be946 100644 --- a/lib/utils/log/writer.go +++ b/lib/utils/log/writer.go @@ -20,7 +20,11 @@ package log import ( "io" + "io/fs" + "os" "sync" + + "github.com/gravitational/trace" ) // SharedWriter is an [io.Writer] implementation that protects @@ -43,3 +47,39 @@ func (s *SharedWriter) Write(p []byte) (int, error) { func NewSharedWriter(w io.Writer) *SharedWriter { return &SharedWriter{Writer: w} } + +// FileSharedWriter is similar to SharedWriter except that it requires a os.File instead of a io.Writer. +// This is to allow the File reopen required by logrotate and similar tools. +// SharedWriter must be used for log destinations that don't have the reopen requirement, like stdout and stderr. +// This is thread safe. +type FileSharedWriter struct { + *os.File + fileFlag int + fileMode fs.FileMode + mu sync.Mutex +} + +func (s *FileSharedWriter) Write(p []byte) (int, error) { + s.mu.Lock() + defer s.mu.Unlock() + + return s.File.Write(p) +} + +// Reopen closes the file and opens it again using APPEND mode. +func (s *FileSharedWriter) Reopen() (err error) { + s.mu.Lock() + defer s.mu.Unlock() + + if err := s.Close(); err != nil { + return trace.Wrap(err) + } + + s.File, err = os.OpenFile(s.Name(), s.fileFlag, s.fileMode) + return trace.Wrap(err) +} + +// NewFileSharedWriter wraps the provided [os.File] in a writer that is thread safe. +func NewFileSharedWriter(f *os.File, flag int, mode fs.FileMode) *FileSharedWriter { + return &FileSharedWriter{File: f, fileFlag: flag, fileMode: mode} +}