github.com/newrelic/newrelic-client-go@v1.1.0/pkg/logs/logs_batch.go (about)

     1  package logs
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  )
     9  
    10  // BatchMode enables the Logs client to accept, queue, and post
    11  // Logs on behalf of the consuming application
    12  func (e *Logs) BatchMode(ctx context.Context, accountID int, opts ...BatchConfigOption) (err error) {
    13  	if e.logQueue != nil {
    14  		return errors.New("the Logs client is already in batch mode")
    15  	}
    16  
    17  	// Loop through config options
    18  	for _, fn := range opts {
    19  		if nil != fn {
    20  			if err := fn(e); err != nil {
    21  				return err
    22  			}
    23  		}
    24  	}
    25  
    26  	e.accountID = accountID
    27  	e.logQueue = make(chan interface{}, e.batchSize)
    28  	e.flushQueue = make([]chan bool, e.batchWorkers)
    29  	e.logTimer = time.NewTimer(e.batchTimeout)
    30  
    31  	// Handle timer based flushing
    32  	go func() {
    33  		err := e.watchdog(ctx)
    34  		if err != nil {
    35  			e.logger.Error(fmt.Sprintf("watchdog returned error: %v", err))
    36  		}
    37  	}()
    38  
    39  	// Spin up some workers
    40  	for x := range e.flushQueue {
    41  		e.flushQueue[x] = make(chan bool, 1)
    42  
    43  		go func(id int) {
    44  			e.logger.Trace("inside anonymous function")
    45  			err := e.batchWorker(ctx, id)
    46  			if err != nil {
    47  				e.logger.Error(fmt.Sprintf("batch worker returned error: %v", err))
    48  			}
    49  		}(x)
    50  	}
    51  
    52  	return nil
    53  }
    54  
    55  type BatchConfigOption func(*Logs) error
    56  
    57  // BatchConfigWorkers sets how many background workers will process
    58  // logs as they are queued
    59  func BatchConfigWorkers(count int) BatchConfigOption {
    60  	return func(e *Logs) error {
    61  		if count <= 0 {
    62  			return errors.New("logs: invalid worker count specified")
    63  		}
    64  
    65  		e.batchWorkers = count
    66  
    67  		return nil
    68  	}
    69  }
    70  
    71  // BatchConfigQueueSize is how many logs to queue before sending
    72  // to New Relic.  If this limit is hit before the Timeout, the queue
    73  // is flushed.
    74  func BatchConfigQueueSize(size int) BatchConfigOption {
    75  	return func(e *Logs) error {
    76  		if size <= 0 {
    77  			return errors.New("logs: invalid queue size specified")
    78  		}
    79  
    80  		e.batchSize = size
    81  		return nil
    82  	}
    83  }
    84  
    85  // BatchConfigTimeout is the maximum amount of time to queue logs
    86  // before sending to New Relic.  If this is reached before the Size
    87  // limit, the queue is flushed.
    88  func BatchConfigTimeout(seconds int) BatchConfigOption {
    89  	return func(e *Logs) error {
    90  		if seconds <= 0 {
    91  			return errors.New("logs: invalid timeout specified")
    92  		}
    93  
    94  		e.batchTimeout = time.Duration(seconds) * time.Second
    95  		return nil
    96  	}
    97  }
    98  
    99  // EnqueueLogEntry handles the queueing. Only works in batch mode. If you wish to be able to avoid blocking
   100  // forever until the log can be queued, provide a ctx with a deadline or timeout as this function will
   101  // bail when ctx.Done() is closed and return and error.
   102  func (e *Logs) EnqueueLogEntry(ctx context.Context, msg interface{}) (err error) {
   103  	if e.logQueue == nil {
   104  		return errors.New("queueing not enabled for this client")
   105  	}
   106  
   107  	select {
   108  	case e.logQueue <- msg:
   109  		e.logger.Trace("EnqueueLogEntry: log entry queued ")
   110  		return nil
   111  	case <-ctx.Done():
   112  		e.logger.Trace("EnqueueLogEntry: exiting per context Done")
   113  		return ctx.Err()
   114  	}
   115  }
   116  
   117  // Flush gives the user a way to manually flush the queue in the foreground.
   118  // This is also used by watchdog when the timer expires.
   119  func (e *Logs) Flush() error {
   120  	if e.flushQueue == nil {
   121  		return errors.New("queueing not enabled for this client")
   122  	}
   123  
   124  	e.logger.Debug("flushing queues")
   125  	for x := range e.flushQueue {
   126  		e.logger.Trace(fmt.Sprintf("flushing logs queue: %d", x))
   127  		e.flushQueue[x] <- true
   128  	}
   129  
   130  	return nil
   131  }
   132  
   133  //
   134  // batchWorker reads []byte from the queue until a threshold is passed,
   135  // then copies the []byte it has read and sends that batch along to Logs
   136  // in its own goroutine.
   137  //
   138  func (e *Logs) batchWorker(ctx context.Context, id int) (err error) {
   139  	e.logger.Trace("batchWorker")
   140  
   141  	if id < 0 || len(e.flushQueue) < id {
   142  		return errors.New("batchWorker: invalid worker id specified")
   143  	}
   144  
   145  	logBuf := make([]interface{}, e.batchSize)
   146  	count := 0
   147  
   148  	for {
   149  		select {
   150  		case item := <-e.logQueue:
   151  			logBuf[count] = item
   152  			count++
   153  			if count >= e.batchSize {
   154  				e.grabAndConsumeLogs(count, logBuf)
   155  				count = 0
   156  			}
   157  		case <-e.flushQueue[id]:
   158  			if count > 0 {
   159  				e.grabAndConsumeLogs(count, logBuf)
   160  				count = 0
   161  			}
   162  		case <-ctx.Done():
   163  			e.logger.Trace("batchWorker[", id, "]: exiting per context Done")
   164  			return ctx.Err()
   165  		}
   166  	}
   167  }
   168  
   169  //
   170  // watchdog has a Timer that will send the results once the
   171  // it has expired.
   172  //
   173  func (e *Logs) watchdog(ctx context.Context) (err error) {
   174  	e.logger.Trace("watchdog")
   175  	if e.logTimer == nil {
   176  		return errors.New("invalid timer for watchdog()")
   177  	}
   178  
   179  	for {
   180  		select {
   181  		case <-e.logTimer.C:
   182  			e.logger.Debug("Timeout expired, flushing queued logs")
   183  			if err = e.Flush(); err != nil {
   184  				return
   185  			}
   186  			e.logTimer.Reset(e.batchTimeout)
   187  		case <-ctx.Done():
   188  			e.logger.Trace("watchdog exiting: context finished")
   189  			return ctx.Err()
   190  		}
   191  	}
   192  }
   193  
   194  // grabAndConsumeLogs makes a copy of the log handles,
   195  // and asynchronously writes those logs in its own goroutine.
   196  func (e *Logs) grabAndConsumeLogs(count int, logBuf []interface{}) {
   197  	e.logger.Trace("grabAndConsumeLogs")
   198  	saved := make([]interface{}, count)
   199  	for i := 0; i < count; i++ {
   200  		saved[i] = logBuf[i]
   201  		logBuf[i] = nil
   202  	}
   203  
   204  	go func(count int, saved []interface{}) {
   205  		if sendErr := e.sendLogs(saved[0:count]); sendErr != nil {
   206  			e.logger.Error("failed to send logs")
   207  		}
   208  	}(count, saved)
   209  }
   210  
   211  func (e *Logs) sendLogs(logs []interface{}) error {
   212  	e.logger.Trace(fmt.Sprintf("sendLogs: entry count: %d", len(logs)))
   213  	_, err := e.client.Post(e.config.Region().LogsURL(), nil, logs, nil)
   214  
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	return nil
   220  }