Skip to content

Commit

Permalink
Re-open the log file on OS Signal trigger
Browse files Browse the repository at this point in the history
This PR implements an handler for the CONT signal.
The handler will close and re-open the log file.
If the log destination is not a file, this is a no-op.

Useful for logrotate postrotate hook.
  • Loading branch information
marcoandredinis committed Jun 21, 2024
1 parent 696fa72 commit bdfa269
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 11 deletions.
1 change: 1 addition & 0 deletions docs/pages/reference/signals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`). |
25 changes: 14 additions & 11 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"crypto/x509"
"errors"
"io"
"io/fs"
"log/slog"
"maps"
"net"
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 "":
Expand Down
5 changes: 5 additions & 0 deletions lib/service/servicecfg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/service/signals.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down
40 changes: 40 additions & 0 deletions lib/utils/log/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ package log

import (
"io"
"io/fs"
"os"
"sync"

"github.com/gravitational/trace"
)

// SharedWriter is an [io.Writer] implementation that protects
Expand All @@ -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}
}

0 comments on commit bdfa269

Please sign in to comment.