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