github.com/manicqin/nomad@v0.9.5/client/logmon/logmon.go (about)

     1  package logmon
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	hclog "github.com/hashicorp/go-hclog"
    12  	"github.com/hashicorp/nomad/client/lib/fifo"
    13  	"github.com/hashicorp/nomad/client/logmon/logging"
    14  )
    15  
    16  const (
    17  	// processOutputCloseTolerance is the length of time we will wait for the
    18  	// launched process to close its stdout/stderr before we force close it. If
    19  	// data is written after this tolerance, we will not capture it.
    20  	processOutputCloseTolerance = 2 * time.Second
    21  )
    22  
    23  type LogConfig struct {
    24  	// LogDir is the host path where logs are to be written to
    25  	LogDir string
    26  
    27  	// StdoutLogFile is the path relative to LogDir for stdout logging
    28  	StdoutLogFile string
    29  
    30  	// StderrLogFile is the path relative to LogDir for stderr logging
    31  	StderrLogFile string
    32  
    33  	// StdoutFifo is the path on the host to the stdout pipe
    34  	StdoutFifo string
    35  
    36  	// StderrFifo is the path on the host to the stderr pipe
    37  	StderrFifo string
    38  
    39  	// MaxFiles is the max rotated files allowed
    40  	MaxFiles int
    41  
    42  	// MaxFileSizeMB is the max log file size in MB allowed before rotation occures
    43  	MaxFileSizeMB int
    44  
    45  	// FileExtension is appended to the end of log files
    46  	FileExtension string
    47  }
    48  
    49  type LogMon interface {
    50  	Start(*LogConfig) error
    51  	Stop() error
    52  }
    53  
    54  func NewLogMon(logger hclog.Logger) LogMon {
    55  	return &logmonImpl{
    56  		logger: logger,
    57  	}
    58  }
    59  
    60  type logmonImpl struct {
    61  	logger hclog.Logger
    62  	tl     *TaskLogger
    63  	lock   sync.Mutex
    64  }
    65  
    66  func (l *logmonImpl) Start(cfg *LogConfig) error {
    67  	l.lock.Lock()
    68  	defer l.lock.Unlock()
    69  
    70  	// first time Start has been called
    71  	if l.tl == nil {
    72  		return l.start(cfg)
    73  	}
    74  
    75  	// stdout and stderr have been closed, this happens during task restarts
    76  	// restart the TaskLogger
    77  	if !l.tl.IsRunning() {
    78  		l.tl.Close()
    79  		return l.start(cfg)
    80  	}
    81  
    82  	// if the TaskLogger has been created and is currently running, noop
    83  	return nil
    84  }
    85  
    86  func (l *logmonImpl) start(cfg *LogConfig) error {
    87  	tl, err := NewTaskLogger(cfg, l.logger)
    88  	if err != nil {
    89  		return err
    90  	}
    91  	l.tl = tl
    92  	return nil
    93  }
    94  
    95  func (l *logmonImpl) Stop() error {
    96  	l.lock.Lock()
    97  	defer l.lock.Unlock()
    98  	if l.tl != nil {
    99  		l.tl.Close()
   100  	}
   101  	return nil
   102  }
   103  
   104  type TaskLogger struct {
   105  	config *LogConfig
   106  
   107  	// rotator for stdout
   108  	lro *logRotatorWrapper
   109  
   110  	// rotator for stderr
   111  	lre *logRotatorWrapper
   112  }
   113  
   114  // IsRunning will return true as long as one rotator wrapper is still running
   115  func (tl *TaskLogger) IsRunning() bool {
   116  	if tl.lro != nil && tl.lro.isRunning() {
   117  		return true
   118  	}
   119  	if tl.lre != nil && tl.lre.isRunning() {
   120  		return true
   121  	}
   122  
   123  	return false
   124  }
   125  
   126  func (tl *TaskLogger) Close() {
   127  	var wg sync.WaitGroup
   128  	if tl.lro != nil {
   129  		wg.Add(1)
   130  		go func() {
   131  			tl.lro.Close()
   132  			wg.Done()
   133  		}()
   134  	}
   135  	if tl.lre != nil {
   136  		wg.Add(1)
   137  		go func() {
   138  			tl.lre.Close()
   139  			wg.Done()
   140  		}()
   141  	}
   142  	wg.Wait()
   143  }
   144  
   145  func NewTaskLogger(cfg *LogConfig, logger hclog.Logger) (*TaskLogger, error) {
   146  	tl := &TaskLogger{config: cfg}
   147  
   148  	logFileSize := int64(cfg.MaxFileSizeMB * 1024 * 1024)
   149  	lro, err := logging.NewFileRotator(cfg.LogDir, cfg.StdoutLogFile,
   150  		cfg.MaxFiles, logFileSize, cfg.FileExtension, logger)
   151  	if err != nil {
   152  		return nil, fmt.Errorf("failed to create stdout logfile for %q: %v", cfg.StdoutLogFile, err)
   153  	}
   154  
   155  	wrapperOut, err := newLogRotatorWrapper(cfg.StdoutFifo, logger, lro)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	tl.lro = wrapperOut
   161  
   162  	lre, err := logging.NewFileRotator(cfg.LogDir, cfg.StderrLogFile,
   163  		cfg.MaxFiles, logFileSize, cfg.FileExtension, logger)
   164  	if err != nil {
   165  		return nil, fmt.Errorf("failed to create stderr logfile for %q: %v", cfg.StderrLogFile, err)
   166  	}
   167  
   168  	wrapperErr, err := newLogRotatorWrapper(cfg.StderrFifo, logger, lre)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	tl.lre = wrapperErr
   174  
   175  	return tl, nil
   176  
   177  }
   178  
   179  // logRotatorWrapper wraps our log rotator and exposes a pipe that can feed the
   180  // log rotator data. The processOutWriter should be attached to the process and
   181  // data will be copied from the reader to the rotator.
   182  type logRotatorWrapper struct {
   183  	fifoPath          string
   184  	rotatorWriter     io.WriteCloser
   185  	hasFinishedCopied chan struct{}
   186  	logger            hclog.Logger
   187  
   188  	processOutReader io.ReadCloser
   189  	openCompleted    chan struct{}
   190  }
   191  
   192  // isRunning will return true until the reader is closed
   193  func (l *logRotatorWrapper) isRunning() bool {
   194  	select {
   195  	case <-l.hasFinishedCopied:
   196  		return false
   197  	default:
   198  		return true
   199  	}
   200  }
   201  
   202  // newLogRotatorWrapper takes a rotator and returns a wrapper that has the
   203  // processOutWriter to attach to the stdout or stderr of a process.
   204  func newLogRotatorWrapper(path string, logger hclog.Logger, rotator io.WriteCloser) (*logRotatorWrapper, error) {
   205  	logger.Info("opening fifo", "path", path)
   206  
   207  	var openFn func() (io.ReadCloser, error)
   208  	var err error
   209  
   210  	_, serr := os.Stat(path)
   211  	if os.IsNotExist(serr) {
   212  		openFn, err = fifo.CreateAndRead(path)
   213  	} else {
   214  		openFn = func() (io.ReadCloser, error) {
   215  			return fifo.OpenReader(path)
   216  		}
   217  	}
   218  
   219  	if err != nil {
   220  		logger.Error("failed to create FIFO", "stat_error", serr, "create_err", err)
   221  		return nil, fmt.Errorf("failed to create fifo for extracting logs: %v", err)
   222  	}
   223  
   224  	wrap := &logRotatorWrapper{
   225  		fifoPath:          path,
   226  		rotatorWriter:     rotator,
   227  		hasFinishedCopied: make(chan struct{}),
   228  		openCompleted:     make(chan struct{}),
   229  		logger:            logger,
   230  	}
   231  
   232  	wrap.start(openFn)
   233  	return wrap, nil
   234  }
   235  
   236  // start starts a goroutine that copies from the pipe into the rotator. This is
   237  // called by the constructor and not the user of the wrapper.
   238  func (l *logRotatorWrapper) start(openFn func() (io.ReadCloser, error)) {
   239  	go func() {
   240  		defer close(l.hasFinishedCopied)
   241  
   242  		reader, err := openFn()
   243  		if err != nil {
   244  			l.logger.Warn("failed to open fifo", "error", err)
   245  			return
   246  		}
   247  		l.processOutReader = reader
   248  		close(l.openCompleted)
   249  
   250  		_, err = io.Copy(l.rotatorWriter, reader)
   251  		if err != nil {
   252  			l.logger.Warn("failed to read from log fifo", "error", err)
   253  			// Close reader to propagate io error across pipe.
   254  			// Note that this may block until the process exits on
   255  			// Windows due to
   256  			// https://github.com/PowerShell/PowerShell/issues/4254
   257  			// or similar issues. Since this is already running in
   258  			// a goroutine its safe to block until the process is
   259  			// force-killed.
   260  			reader.Close()
   261  		}
   262  	}()
   263  	return
   264  }
   265  
   266  // Close closes the rotator and the process writer to ensure that the Wait
   267  // command exits.
   268  func (l *logRotatorWrapper) Close() {
   269  	// Wait up to the close tolerance before we force close
   270  	select {
   271  	case <-l.hasFinishedCopied:
   272  	case <-time.After(processOutputCloseTolerance):
   273  	}
   274  
   275  	// Closing the read side of a pipe may block on Windows if the process
   276  	// is being debugged as in:
   277  	// https://github.com/PowerShell/PowerShell/issues/4254
   278  	// The pipe will be closed and cleaned up when the process exits.
   279  	closeDone := make(chan struct{})
   280  	go func() {
   281  		defer close(closeDone)
   282  
   283  		// we must wait until reader is opened before we can close it, and cannot inteerrupt an in-flight open request
   284  		// The Close function uses processOutputCloseTolerance to protect against long running open called
   285  		// and then request will be interrupted and file will be closed on process shutdown
   286  		<-l.openCompleted
   287  
   288  		if l.processOutReader != nil {
   289  			err := l.processOutReader.Close()
   290  			if err != nil && !strings.Contains(err.Error(), "file already closed") {
   291  				l.logger.Warn("error closing read-side of process output pipe", "err", err)
   292  			}
   293  		}
   294  
   295  	}()
   296  
   297  	select {
   298  	case <-closeDone:
   299  	case <-time.After(processOutputCloseTolerance):
   300  		l.logger.Warn("timed out waiting for read-side of process output pipe to close")
   301  	}
   302  
   303  	l.rotatorWriter.Close()
   304  }