github.com/nsqio/nsq@v1.3.0/apps/nsq_to_file/file_logger.go (about)

     1  package main
     2  
     3  import (
     4  	"compress/gzip"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/nsqio/go-nsq"
    15  	"github.com/nsqio/nsq/internal/lg"
    16  )
    17  
    18  type FileLogger struct {
    19  	logf     lg.AppLogFunc
    20  	opts     *Options
    21  	topic    string
    22  	consumer *nsq.Consumer
    23  
    24  	out            *os.File
    25  	writer         io.Writer
    26  	gzipWriter     *gzip.Writer
    27  	logChan        chan *nsq.Message
    28  	filenameFormat string
    29  
    30  	termChan chan bool
    31  	hupChan  chan bool
    32  
    33  	// for rotation
    34  	filename string
    35  	openTime time.Time
    36  	filesize int64
    37  	rev      uint
    38  }
    39  
    40  func NewFileLogger(logf lg.AppLogFunc, opts *Options, topic string, cfg *nsq.Config) (*FileLogger, error) {
    41  	computedFilenameFormat, err := computeFilenameFormat(opts, topic)
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  
    46  	consumer, err := nsq.NewConsumer(topic, opts.Channel, cfg)
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  
    51  	f := &FileLogger{
    52  		logf:           logf,
    53  		opts:           opts,
    54  		topic:          topic,
    55  		consumer:       consumer,
    56  		logChan:        make(chan *nsq.Message, 1),
    57  		filenameFormat: computedFilenameFormat,
    58  		termChan:       make(chan bool),
    59  		hupChan:        make(chan bool),
    60  	}
    61  	consumer.AddHandler(f)
    62  
    63  	err = consumer.ConnectToNSQDs(opts.NSQDTCPAddrs)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	err = consumer.ConnectToNSQLookupds(opts.NSQLookupdHTTPAddrs)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	return f, nil
    74  }
    75  
    76  func (f *FileLogger) HandleMessage(m *nsq.Message) error {
    77  	m.DisableAutoResponse()
    78  	f.logChan <- m
    79  	return nil
    80  }
    81  
    82  func (f *FileLogger) router() {
    83  	pos := 0
    84  	output := make([]*nsq.Message, f.opts.MaxInFlight)
    85  	sync := false
    86  	ticker := time.NewTicker(f.opts.SyncInterval)
    87  	closeFile := false
    88  	exit := false
    89  
    90  	for {
    91  		select {
    92  		case <-f.consumer.StopChan:
    93  			sync = true
    94  			closeFile = true
    95  			exit = true
    96  		case <-f.termChan:
    97  			ticker.Stop()
    98  			f.consumer.Stop()
    99  			sync = true
   100  		case <-f.hupChan:
   101  			sync = true
   102  			closeFile = true
   103  		case <-ticker.C:
   104  			if f.needsRotation() {
   105  				if f.opts.SkipEmptyFiles {
   106  					closeFile = true
   107  				} else {
   108  					f.updateFile()
   109  				}
   110  			}
   111  			sync = true
   112  		case m := <-f.logChan:
   113  			if f.needsRotation() {
   114  				f.updateFile()
   115  				sync = true
   116  			}
   117  			_, err := f.Write(m.Body)
   118  			if err != nil {
   119  				f.logf(lg.FATAL, "[%s/%s] writing message to disk: %s", f.topic, f.opts.Channel, err)
   120  				os.Exit(1)
   121  			}
   122  			_, err = f.Write([]byte("\n"))
   123  			if err != nil {
   124  				f.logf(lg.FATAL, "[%s/%s] writing newline to disk: %s", f.topic, f.opts.Channel, err)
   125  				os.Exit(1)
   126  			}
   127  			output[pos] = m
   128  			pos++
   129  			if pos == cap(output) {
   130  				sync = true
   131  			}
   132  		}
   133  
   134  		if sync || f.consumer.IsStarved() {
   135  			if pos > 0 {
   136  				f.logf(lg.INFO, "[%s/%s] syncing %d records to disk", f.topic, f.opts.Channel, pos)
   137  				err := f.Sync()
   138  				if err != nil {
   139  					f.logf(lg.FATAL, "[%s/%s] failed syncing messages: %s", f.topic, f.opts.Channel, err)
   140  					os.Exit(1)
   141  				}
   142  				for pos > 0 {
   143  					pos--
   144  					m := output[pos]
   145  					m.Finish()
   146  					output[pos] = nil
   147  				}
   148  			}
   149  			sync = false
   150  		}
   151  
   152  		if closeFile {
   153  			f.Close()
   154  			closeFile = false
   155  		}
   156  
   157  		if exit {
   158  			break
   159  		}
   160  	}
   161  }
   162  
   163  func (f *FileLogger) Close() {
   164  	if f.out == nil {
   165  		return
   166  	}
   167  
   168  	if f.gzipWriter != nil {
   169  		err := f.gzipWriter.Close()
   170  		if err != nil {
   171  			f.logf(lg.FATAL, "[%s/%s] failed to close GZIP writer: %s", f.topic, f.opts.Channel, err)
   172  			os.Exit(1)
   173  		}
   174  	}
   175  	err := f.out.Sync()
   176  	if err != nil {
   177  		f.logf(lg.FATAL, "[%s/%s] failed to fsync output file: %s", f.topic, f.opts.Channel, err)
   178  		os.Exit(1)
   179  	}
   180  	err = f.out.Close()
   181  	if err != nil {
   182  		f.logf(lg.FATAL, "[%s/%s] failed to close output file: %s", f.topic, f.opts.Channel, err)
   183  		os.Exit(1)
   184  	}
   185  
   186  	// Move file from work dir to output dir if necessary, taking care not
   187  	// to overwrite existing files
   188  	if f.opts.WorkDir != f.opts.OutputDir {
   189  		src := f.out.Name()
   190  		dst := filepath.Join(f.opts.OutputDir, strings.TrimPrefix(src, f.opts.WorkDir))
   191  
   192  		// Optimistic rename
   193  		f.logf(lg.INFO, "[%s/%s] moving finished file %s to %s", f.topic, f.opts.Channel, src, dst)
   194  		err := exclusiveRename(src, dst)
   195  		if err == nil {
   196  			return
   197  		} else if !os.IsExist(err) {
   198  			f.logf(lg.FATAL, "[%s/%s] unable to move file from %s to %s: %s", f.topic, f.opts.Channel, src, dst, err)
   199  			os.Exit(1)
   200  		}
   201  
   202  		// Optimistic rename failed, so we need to generate a new
   203  		// destination file name by bumping the revision number.
   204  		_, filenameTmpl := filepath.Split(f.filename)
   205  		dstDir, _ := filepath.Split(dst)
   206  		dstTmpl := filepath.Join(dstDir, filenameTmpl)
   207  
   208  		for i := f.rev + 1; ; i++ {
   209  			f.logf(lg.WARN, "[%s/%s] destination file already exists: %s", f.topic, f.opts.Channel, dst)
   210  			dst := strings.Replace(dstTmpl, "<REV>", fmt.Sprintf("-%06d", i), -1)
   211  			err := exclusiveRename(src, dst)
   212  			if err != nil {
   213  				if os.IsExist(err) {
   214  					continue // next rev
   215  				}
   216  				f.logf(lg.FATAL, "[%s/%s] unable to rename file from %s to %s: %s", f.topic, f.opts.Channel, src, dst, err)
   217  				os.Exit(1)
   218  			}
   219  			f.logf(lg.INFO, "[%s/%s] renamed finished file %s to %s to avoid overwrite", f.topic, f.opts.Channel, src, dst)
   220  			break
   221  		}
   222  	}
   223  
   224  	f.out = nil
   225  }
   226  
   227  func (f *FileLogger) Write(p []byte) (int, error) {
   228  	n, err := f.writer.Write(p)
   229  	f.filesize += int64(n)
   230  	return n, err
   231  }
   232  
   233  func (f *FileLogger) Sync() error {
   234  	var err error
   235  	if f.gzipWriter != nil {
   236  		// finish current gzip stream and start a new one (concatenated)
   237  		// gzip stream trailer has checksum, and can indicate which messages were ACKed
   238  		err = f.gzipWriter.Close()
   239  		if err != nil {
   240  			return err
   241  		}
   242  		err = f.out.Sync()
   243  		f.gzipWriter, _ = gzip.NewWriterLevel(f.out, f.opts.GZIPLevel)
   244  		f.writer = f.gzipWriter
   245  	} else {
   246  		err = f.out.Sync()
   247  	}
   248  	return err
   249  }
   250  
   251  func (f *FileLogger) currentFilename() string {
   252  	t := time.Now()
   253  	datetime := strftime(f.opts.DatetimeFormat, t)
   254  	return strings.Replace(f.filenameFormat, "<DATETIME>", datetime, -1)
   255  }
   256  
   257  func (f *FileLogger) needsRotation() bool {
   258  	if f.out == nil {
   259  		return true
   260  	}
   261  
   262  	filename := f.currentFilename()
   263  	if filename != f.filename {
   264  		f.logf(lg.INFO, "[%s/%s] new filename %s, rotating...", f.topic, f.opts.Channel, filename)
   265  		return true // rotate by filename
   266  	}
   267  
   268  	if f.opts.RotateInterval > 0 {
   269  		if s := time.Since(f.openTime); s > f.opts.RotateInterval {
   270  			f.logf(lg.INFO, "[%s/%s] %s since last open, rotating...", f.topic, f.opts.Channel, s)
   271  			return true // rotate by interval
   272  		}
   273  	}
   274  
   275  	if f.opts.RotateSize > 0 && f.filesize > f.opts.RotateSize {
   276  		f.logf(lg.INFO, "[%s/%s] %s currently %d bytes (> %d), rotating...",
   277  			f.topic, f.opts.Channel, f.out.Name(), f.filesize, f.opts.RotateSize)
   278  		return true // rotate by size
   279  	}
   280  
   281  	return false
   282  }
   283  
   284  func (f *FileLogger) updateFile() {
   285  	f.Close() // uses current f.filename and f.rev to resolve rename dst conflict
   286  
   287  	filename := f.currentFilename()
   288  	if filename != f.filename {
   289  		f.rev = 0 // reset revision to 0 if it is a new filename
   290  	} else {
   291  		f.rev++
   292  	}
   293  	f.filename = filename
   294  	f.openTime = time.Now()
   295  
   296  	fullPath := path.Join(f.opts.WorkDir, filename)
   297  	err := makeDirFromPath(f.logf, fullPath)
   298  	if err != nil {
   299  		f.logf(lg.FATAL, "[%s/%s] unable to create dir: %s", f.topic, f.opts.Channel, err)
   300  		os.Exit(1)
   301  	}
   302  
   303  	var fi os.FileInfo
   304  	for ; ; f.rev++ {
   305  		absFilename := strings.Replace(fullPath, "<REV>", fmt.Sprintf("-%06d", f.rev), -1)
   306  
   307  		// If we're using a working directory for in-progress files,
   308  		// proactively check for duplicate file names in the output dir to
   309  		// prevent conflicts on rename in the normal case
   310  		if f.opts.WorkDir != f.opts.OutputDir {
   311  			outputFileName := filepath.Join(f.opts.OutputDir, strings.TrimPrefix(absFilename, f.opts.WorkDir))
   312  			err := makeDirFromPath(f.logf, outputFileName)
   313  			if err != nil {
   314  				f.logf(lg.FATAL, "[%s/%s] unable to create dir: %s", f.topic, f.opts.Channel, err)
   315  				os.Exit(1)
   316  			}
   317  
   318  			_, err = os.Stat(outputFileName)
   319  			if err == nil {
   320  				f.logf(lg.WARN, "[%s/%s] output file already exists: %s", f.topic, f.opts.Channel, outputFileName)
   321  				continue // next rev
   322  			} else if !os.IsNotExist(err) {
   323  				f.logf(lg.FATAL, "[%s/%s] unable to stat output file %s: %s", f.topic, f.opts.Channel, outputFileName, err)
   324  				os.Exit(1)
   325  			}
   326  		}
   327  
   328  		openFlag := os.O_WRONLY | os.O_CREATE
   329  		if f.opts.GZIP || f.opts.RotateInterval > 0 {
   330  			openFlag |= os.O_EXCL
   331  		} else {
   332  			openFlag |= os.O_APPEND
   333  		}
   334  		f.out, err = os.OpenFile(absFilename, openFlag, 0666)
   335  		if err != nil {
   336  			if os.IsExist(err) {
   337  				f.logf(lg.WARN, "[%s/%s] working file already exists: %s", f.topic, f.opts.Channel, absFilename)
   338  				continue // next rev
   339  			}
   340  			f.logf(lg.FATAL, "[%s/%s] unable to open %s: %s", f.topic, f.opts.Channel, absFilename, err)
   341  			os.Exit(1)
   342  		}
   343  
   344  		f.logf(lg.INFO, "[%s/%s] opening %s", f.topic, f.opts.Channel, absFilename)
   345  
   346  		fi, err = f.out.Stat()
   347  		if err != nil {
   348  			f.logf(lg.FATAL, "[%s/%s] unable to stat file %s: %s", f.topic, f.opts.Channel, f.out.Name(), err)
   349  		}
   350  		f.filesize = fi.Size()
   351  
   352  		if f.opts.RotateSize > 0 && f.filesize > f.opts.RotateSize {
   353  			f.logf(lg.INFO, "[%s/%s] %s currently %d bytes (> %d), rotating...",
   354  				f.topic, f.opts.Channel, f.out.Name(), f.filesize, f.opts.RotateSize)
   355  			continue // next rev
   356  		}
   357  
   358  		break // good file
   359  	}
   360  
   361  	if f.opts.GZIP {
   362  		f.gzipWriter, _ = gzip.NewWriterLevel(f.out, f.opts.GZIPLevel)
   363  		f.writer = f.gzipWriter
   364  	} else {
   365  		f.writer = f.out
   366  	}
   367  }
   368  
   369  func makeDirFromPath(logf lg.AppLogFunc, path string) error {
   370  	dir, _ := filepath.Split(path)
   371  	if dir != "" {
   372  		return os.MkdirAll(dir, 0770)
   373  	}
   374  	return nil
   375  }
   376  
   377  func exclusiveRename(src, dst string) error {
   378  	err := os.Link(src, dst)
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	err = os.Remove(src)
   384  	if err != nil {
   385  		return err
   386  	}
   387  
   388  	return nil
   389  }
   390  
   391  func computeFilenameFormat(opts *Options, topic string) (string, error) {
   392  	hostname, err := os.Hostname()
   393  	if err != nil {
   394  		return "", err
   395  	}
   396  	shortHostname := strings.Split(hostname, ".")[0]
   397  
   398  	identifier := shortHostname
   399  	if len(opts.HostIdentifier) != 0 {
   400  		identifier = strings.Replace(opts.HostIdentifier, "<SHORT_HOST>", shortHostname, -1)
   401  		identifier = strings.Replace(identifier, "<HOSTNAME>", hostname, -1)
   402  	}
   403  
   404  	cff := opts.FilenameFormat
   405  	if opts.GZIP || opts.RotateSize > 0 || opts.RotateInterval > 0 || opts.WorkDir != opts.OutputDir {
   406  		if !strings.Contains(cff, "<REV>") {
   407  			return "", errors.New("missing <REV> in --filename-format when gzip, rotation, or work dir enabled")
   408  		}
   409  	} else {
   410  		// remove <REV> as we don't need it
   411  		cff = strings.Replace(cff, "<REV>", "", -1)
   412  	}
   413  	cff = strings.Replace(cff, "<TOPIC>", topic, -1)
   414  	cff = strings.Replace(cff, "<HOST>", identifier, -1)
   415  	cff = strings.Replace(cff, "<PID>", fmt.Sprintf("%d", os.Getpid()), -1)
   416  	if opts.GZIP && !strings.HasSuffix(cff, ".gz") {
   417  		cff = cff + ".gz"
   418  	}
   419  
   420  	return cff, nil
   421  }