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 }