github.com/Cloud-Foundations/Dominator@v0.3.4/lib/logbuf/impl.go (about)

     1  package logbuf
     2  
     3  import (
     4  	"bufio"
     5  	"container/ring"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"html"
    10  	"io"
    11  	"os"
    12  	"path"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/Cloud-Foundations/Dominator/lib/bufwriter"
    18  	"github.com/Cloud-Foundations/Dominator/lib/flagutil"
    19  	"github.com/Cloud-Foundations/Dominator/lib/format"
    20  	"github.com/Cloud-Foundations/Dominator/lib/fsutil"
    21  	"github.com/Cloud-Foundations/Dominator/lib/wsyscall"
    22  )
    23  
    24  const (
    25  	reopenMessage = "Closing and will open new logfile"
    26  	timeLayout    = "2006-01-02:15:04:05.999"
    27  )
    28  
    29  func newLogBuffer(options Options) *LogBuffer {
    30  	if options.MaxBufferLines < 100 {
    31  		options.MaxBufferLines = 100
    32  	}
    33  	if options.MaxFileSize < 16384 {
    34  		options.MaxFileSize = 16384
    35  	}
    36  	if options.Quota < 65536 {
    37  		options.Quota = 65536
    38  	}
    39  	if options.AlsoLogToStderr {
    40  		options.RedirectStderr = false // Prevent duplicates sent to stderr.
    41  	}
    42  	logBuffer := &LogBuffer{
    43  		options: options,
    44  		buffer:  ring.New(int(options.MaxBufferLines)),
    45  	}
    46  	if err := logBuffer.setupFileLogging(); err != nil {
    47  		fmt.Fprintln(logBuffer, err)
    48  	}
    49  	logBuffer.addHttpHandlers()
    50  	return logBuffer
    51  }
    52  
    53  func (lb *LogBuffer) setupFileLogging() error {
    54  	if lb.options.Directory == "" {
    55  		return nil
    56  	}
    57  	if err := lb.createLogDirectory(); err != nil {
    58  		return err
    59  	}
    60  	writeNotifier := make(chan struct{}, 1)
    61  	lb.writeNotifier = writeNotifier
    62  	go lb.idleMarker(writeNotifier)
    63  	return nil
    64  }
    65  
    66  func (lb *LogBuffer) checkNeverLogged() bool {
    67  	lb.rwMutex.Lock()
    68  	defer lb.rwMutex.Unlock()
    69  	return lb.noLogsEver
    70  }
    71  
    72  func (lb *LogBuffer) createLogDirectory() error {
    73  	if fi, err := os.Stat(lb.options.Directory); err != nil {
    74  		if err := os.Mkdir(lb.options.Directory, dirPerms); err != nil {
    75  			return fmt.Errorf("error creating: %s: %s",
    76  				lb.options.Directory, err)
    77  		}
    78  		lb.noLogsEver = true
    79  	} else if !fi.IsDir() {
    80  		return errors.New(lb.options.Directory + ": is not a directory")
    81  	} else {
    82  		names, err := fsutil.ReadDirnames(lb.options.Directory, false)
    83  		if err != nil {
    84  			return err
    85  		} else if len(names) < 1 {
    86  			lb.noLogsEver = true
    87  		}
    88  	}
    89  	lb.scanPreviousForPanic()
    90  	return lb.enforceQuota()
    91  }
    92  
    93  func (lb *LogBuffer) scanPreviousForPanic() {
    94  	target, err := os.Readlink(path.Join(lb.options.Directory, "latest"))
    95  	if err != nil {
    96  		return
    97  	}
    98  	targetPath := path.Join(lb.options.Directory, target)
    99  	file, err := os.Open(targetPath)
   100  	if err != nil {
   101  		return
   102  	}
   103  	go func() {
   104  		defer file.Close()
   105  		scanner := bufio.NewScanner(file)
   106  		var previousLineHadPanicString bool
   107  		for scanner.Scan() {
   108  			line := scanner.Text()
   109  			var looksLikeAPanic bool
   110  			if strings.HasPrefix(line, "panic: ") {
   111  				looksLikeAPanic = true
   112  			} else if strings.HasPrefix(line, "goroutine ") {
   113  				if previousLineHadPanicString {
   114  					looksLikeAPanic = true
   115  				}
   116  			} else if strings.Contains(line, "panic") {
   117  				previousLineHadPanicString = true
   118  			} else {
   119  				previousLineHadPanicString = false
   120  			}
   121  			if looksLikeAPanic {
   122  				lb.rwMutex.Lock()
   123  				lb.panicLogfile = &target
   124  				lb.rwMutex.Unlock()
   125  				if fi, err := os.Stat(targetPath); err != nil {
   126  					return
   127  				} else {
   128  					os.Chmod(targetPath, fi.Mode()|os.ModeSticky)
   129  				}
   130  				return
   131  			}
   132  		}
   133  	}()
   134  }
   135  
   136  func (lb *LogBuffer) dump(writer io.Writer, prefix, postfix string,
   137  	recentFirst bool, escapeHtml bool) error {
   138  	entries := lb.getEntries()
   139  	if recentFirst {
   140  		reverseEntries(entries)
   141  	}
   142  	for _, entry := range entries {
   143  		writer.Write([]byte(prefix))
   144  		if escapeHtml {
   145  			writer.Write([]byte(html.EscapeString(string(entry))))
   146  		} else {
   147  			writer.Write(entry)
   148  		}
   149  		writer.Write([]byte(postfix))
   150  	}
   151  	return nil
   152  }
   153  
   154  func (lb *LogBuffer) flush() error {
   155  	lb.rwMutex.Lock()
   156  	defer lb.rwMutex.Unlock()
   157  	if lb.writer != nil {
   158  		return lb.writer.Flush()
   159  	}
   160  	return nil
   161  }
   162  
   163  func (lb *LogBuffer) write(p []byte) (n int, err error) {
   164  	if lb.options.AlsoLogToStderr {
   165  		os.Stderr.Write(p)
   166  	}
   167  	val := make([]byte, len(p))
   168  	copy(val, p)
   169  	lb.rwMutex.Lock()
   170  	sendNotify := lb.writeToLogFile(p)
   171  	lb.buffer.Value = val
   172  	lb.buffer = lb.buffer.Next()
   173  	lb.rwMutex.Unlock()
   174  	if sendNotify {
   175  		lb.writeNotifier <- struct{}{}
   176  	}
   177  	return len(p), nil
   178  }
   179  
   180  // This should be called with the lock held.
   181  func (lb *LogBuffer) writeToLogFile(p []byte) bool {
   182  	if lb.writer == nil {
   183  		return false
   184  	}
   185  	lb.writer.Write(p)
   186  	lb.fileSize += flagutil.Size(len(p))
   187  	if lb.fileSize > lb.options.MaxFileSize {
   188  		lb.closeAndOpenNewFile()
   189  	}
   190  	lb.usage += flagutil.Size(len(p))
   191  	if lb.usage > lb.options.Quota {
   192  		lb.enforceQuota()
   193  	}
   194  	return true
   195  }
   196  
   197  // This should be called with the lock held.
   198  func (lb *LogBuffer) closeAndOpenNewFile() error {
   199  	now := time.Now()
   200  	year, month, day := now.Date()
   201  	hour, minute, second := now.Clock()
   202  	nWritten, _ := fmt.Fprintf(lb.writer, "%d/%02d/%02d %02d:%02d:%02d %s\n",
   203  		year, month, day, hour, minute, second, reopenMessage)
   204  	lb.usage += flagutil.Size(nWritten)
   205  	lb.writer.Flush()
   206  	lb.writer = nil
   207  	lb.file.Close()
   208  	if err := lb.openNewFile(); err != nil {
   209  		return err
   210  	}
   211  	return nil
   212  }
   213  
   214  // This should be called with the lock held.
   215  func (lb *LogBuffer) openNewFile() error {
   216  	lb.fileSize = 0
   217  	filename := time.Now().Format(timeLayout)
   218  	file, err := os.OpenFile(path.Join(lb.options.Directory, filename),
   219  		os.O_CREATE|os.O_WRONLY, filePerms)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	if lb.firstFile == "" {
   224  		lb.firstFile = filename
   225  	}
   226  	if lb.options.RedirectStderr {
   227  		wsyscall.Dup2(int(file.Fd()), int(os.Stdout.Fd()))
   228  		wsyscall.Dup2(int(file.Fd()), int(os.Stderr.Fd()))
   229  	}
   230  	lb.file = file
   231  	lb.writer = bufwriter.NewWriter(file, time.Second)
   232  	symlink := path.Join(lb.options.Directory, "latest")
   233  	tmpSymlink := symlink + "~"
   234  	os.Remove(tmpSymlink)
   235  	os.Symlink(filename, tmpSymlink)
   236  	return os.Rename(tmpSymlink, symlink)
   237  }
   238  
   239  // This should be called with the lock held.
   240  func (lb *LogBuffer) enforceQuota() error {
   241  	file, err := os.Open(lb.options.Directory)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	names, err := file.Readdirnames(-1)
   246  	file.Close()
   247  	if err != nil {
   248  		return err
   249  	}
   250  	sort.Strings(names)
   251  	var numBytesDeleted, numFilesDeleted uint64
   252  	var usage flagutil.Size
   253  	deletedLatestFile := false
   254  	deleteRemainingFiles := false
   255  	latestFile := true
   256  	for index := len(names) - 1; index >= 0; index-- {
   257  		filename := path.Join(lb.options.Directory, names[index])
   258  		fi, err := os.Lstat(filename)
   259  		if err == os.ErrNotExist {
   260  			continue
   261  		}
   262  		if err != nil {
   263  			return err
   264  		}
   265  		if fi.Mode().IsRegular() {
   266  			size := flagutil.Size(fi.Size())
   267  			if size < lb.options.Quota>>10 {
   268  				size = lb.options.Quota >> 10 // Limit number of files to 1024.
   269  			}
   270  			if size+usage > lb.options.Quota || deleteRemainingFiles {
   271  				os.Remove(filename)
   272  				numBytesDeleted += uint64(fi.Size())
   273  				numFilesDeleted++
   274  				deleteRemainingFiles = true
   275  				if latestFile {
   276  					deletedLatestFile = true
   277  				}
   278  			} else {
   279  				usage += size
   280  			}
   281  			latestFile = false
   282  		}
   283  	}
   284  	lb.usage = usage
   285  	if deletedLatestFile && lb.file != nil {
   286  		lb.writer.Flush()
   287  		lb.writer = nil
   288  		lb.file.Close()
   289  		lb.file = nil
   290  	}
   291  	if lb.file == nil {
   292  		if err := lb.openNewFile(); err != nil {
   293  			return err
   294  		}
   295  	}
   296  	if numBytesDeleted > 0 {
   297  		now := time.Now()
   298  		year, month, day := now.Date()
   299  		hour, minute, second := now.Clock()
   300  		nWritten, _ := fmt.Fprintf(lb.writer,
   301  			"%d/%02d/%02d %02d:%02d:%02d Deleted %s in %d files\n",
   302  			year, month, day, hour, minute, second,
   303  			format.FormatBytes(numBytesDeleted), numFilesDeleted)
   304  		lb.fileSize += flagutil.Size(nWritten)
   305  		lb.usage += flagutil.Size(nWritten)
   306  	}
   307  	return nil
   308  }
   309  
   310  func (lb *LogBuffer) idleMarker(writeNotifier <-chan struct{}) {
   311  	idleMarkDuration := lb.options.IdleMarkTimeout
   312  	if idleMarkDuration < 1 {
   313  		for {
   314  			<-writeNotifier
   315  		}
   316  	}
   317  	idleMarkTimer := time.NewTimer(idleMarkDuration)
   318  	for {
   319  		select {
   320  		case <-writeNotifier:
   321  			idleMarkTimer.Reset(idleMarkDuration)
   322  		case <-idleMarkTimer.C:
   323  			lb.writeMark()
   324  			idleMarkTimer.Reset(idleMarkDuration)
   325  		}
   326  	}
   327  }
   328  
   329  func (lb *LogBuffer) getEntries() [][]byte {
   330  	lb.rwMutex.RLock()
   331  	defer lb.rwMutex.RUnlock()
   332  	entries := make([][]byte, 0, lb.buffer.Len())
   333  	lb.buffer.Do(func(p interface{}) {
   334  		if p != nil {
   335  			entries = append(entries, p.([]byte))
   336  		}
   337  	})
   338  	return entries
   339  }
   340  
   341  func (lb *LogBuffer) dumpSince(writer io.Writer, name string,
   342  	earliestTime time.Time, prefix, postfix string, recentFirst bool) (
   343  	bool, error) {
   344  	file, err := os.Open(path.Join(lb.options.Directory,
   345  		path.Base(path.Clean(name))))
   346  	if err != nil {
   347  		return false, err
   348  	}
   349  	defer file.Close()
   350  	scanner := bufio.NewScanner(file)
   351  	lines := make([]string, 0)
   352  	timeFormat := "2006/01/02 15:04:05"
   353  	minLength := len(timeFormat) + 2
   354  	foundReopenMessage := false
   355  	for scanner.Scan() {
   356  		line := scanner.Text()
   357  		if strings.Contains(line, reopenMessage) {
   358  			foundReopenMessage = true
   359  			continue
   360  		}
   361  		if len(line) >= minLength {
   362  			timeString := line[:minLength-2]
   363  			timeStamp, err := time.ParseInLocation(timeFormat, timeString,
   364  				time.Local)
   365  			if err == nil && timeStamp.Before(earliestTime) {
   366  				continue
   367  			}
   368  		}
   369  		if recentFirst {
   370  			lines = append(lines, line)
   371  		} else {
   372  			writer.Write([]byte(prefix))
   373  			writer.Write([]byte(line))
   374  			writer.Write([]byte(postfix))
   375  		}
   376  	}
   377  	if err := scanner.Err(); err != nil {
   378  		return false, err
   379  	}
   380  	if recentFirst {
   381  		reverseStrings(lines)
   382  		for _, line := range lines {
   383  			writer.Write([]byte(prefix))
   384  			writer.Write([]byte(line))
   385  			writer.Write([]byte(postfix))
   386  		}
   387  	}
   388  	return foundReopenMessage, nil
   389  }
   390  
   391  func (lb *LogBuffer) writeMark() {
   392  	now := time.Now()
   393  	year, month, day := now.Date()
   394  	hour, minute, second := now.Clock()
   395  	str := fmt.Sprintf("%d/%02d/%02d %02d:%02d:%02d MARK\n",
   396  		year, month, day, hour, minute, second)
   397  	lb.rwMutex.Lock()
   398  	defer lb.rwMutex.Unlock()
   399  	lb.writeToLogFile([]byte(str))
   400  }
   401  
   402  func reverseEntries(entries [][]byte) {
   403  	length := len(entries)
   404  	for index := 0; index < length/2; index++ {
   405  		entries[index], entries[length-1-index] =
   406  			entries[length-1-index], entries[index]
   407  	}
   408  }
   409  
   410  func reverseStrings(strings []string) {
   411  	length := len(strings)
   412  	for index := 0; index < length/2; index++ {
   413  		strings[index], strings[length-1-index] =
   414  			strings[length-1-index], strings[index]
   415  	}
   416  }
   417  
   418  func init() {
   419  	UseFlagSet(flag.CommandLine)
   420  }