github.com/newrelic/newrelic-client-go@v1.1.0/pkg/events/events_batch.go (about) 1 package events 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "time" 8 ) 9 10 // BatchMode enables the Events client to accept, queue, and post 11 // Events on behalf of the consuming application 12 func (e *Events) BatchMode(ctx context.Context, accountID int, opts ...BatchConfigOption) (err error) { 13 if e.eventQueue != nil { 14 return errors.New("the Events 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.eventQueue = make(chan []byte, e.batchSize) 28 e.flushQueue = make([]chan bool, e.batchWorkers) 29 e.eventTimer = 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("watchdog returned error", "error", 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 err := e.batchWorker(ctx, id) 45 if err != nil { 46 e.logger.Error("batch worker returned error", "error", err) 47 } 48 }(x) 49 } 50 51 return nil 52 } 53 54 type BatchConfigOption func(*Events) error 55 56 // BatchConfigWorkers sets how many background workers will process 57 // events as they are queued 58 func BatchConfigWorkers(count int) BatchConfigOption { 59 return func(e *Events) error { 60 if count <= 0 { 61 return errors.New("events: invalid worker count specified") 62 } 63 64 e.batchWorkers = count 65 66 return nil 67 } 68 } 69 70 // BatchConfigQueueSize is how many events to queue before sending 71 // to New Relic. If this limit is hit before the Timeout, the queue 72 // is flushed. 73 func BatchConfigQueueSize(size int) BatchConfigOption { 74 return func(e *Events) error { 75 if size <= 0 { 76 return errors.New("events: invalid queue size specified") 77 } 78 79 e.batchSize = size 80 return nil 81 } 82 } 83 84 // BatchConfigTimeout is the maximum amount of time to queue events 85 // before sending to New Relic. If this is reached before the Size 86 // limit, the queue is flushed. 87 func BatchConfigTimeout(seconds int) BatchConfigOption { 88 return func(e *Events) error { 89 if seconds <= 0 { 90 return errors.New("events: invalid timeout specified") 91 } 92 93 e.batchTimeout = time.Duration(seconds) * time.Second 94 return nil 95 } 96 } 97 98 // EnqueueEventContext handles the queueing. Only works in batch mode. If you wish to be able to avoid blocking 99 // forever until the event can be queued, provide a ctx with a deadline or timeout as this function will 100 // bail when ctx.Done() is closed and return and error. 101 func (e *Events) EnqueueEvent(ctx context.Context, event interface{}) (err error) { 102 if e.eventQueue == nil { 103 return errors.New("queueing not enabled for this client") 104 } 105 106 jsonData, err := e.marshalEvent(event) 107 if err != nil { 108 return err 109 } 110 if jsonData == nil { 111 return errors.New("events: EnqueueEvent marhal returned nil data") 112 } 113 114 select { 115 case e.eventQueue <- *jsonData: 116 return nil 117 case <-ctx.Done(): 118 e.logger.Trace("EnqueueEvent: exiting per context Done") 119 return ctx.Err() 120 } 121 } 122 123 // Flush gives the user a way to manually flush the queue in the foreground. 124 // This is also used by watchdog when the timer expires. 125 func (e *Events) Flush() error { 126 if e.flushQueue == nil { 127 return errors.New("queueing not enabled for this client") 128 } 129 130 e.logger.Debug("flushing events") 131 132 for x := range e.flushQueue { 133 e.flushQueue[x] <- true 134 } 135 136 return nil 137 } 138 139 // 140 // batchWorker reads []byte from the queue until a threshold is passed, 141 // then copies the []byte it has read and sends that batch along to Insights 142 // in its own goroutine. 143 // 144 func (e *Events) batchWorker(ctx context.Context, id int) (err error) { 145 if e == nil { 146 return errors.New("batchWorker: invalid Events, unable to start worker") 147 } 148 if id < 0 || len(e.flushQueue) < id { 149 return errors.New("batchWorker: invalid worker id specified") 150 } 151 152 eventBuf := make([][]byte, e.batchSize) 153 count := 0 154 155 for { 156 select { 157 case item := <-e.eventQueue: 158 eventBuf[count] = item 159 count++ 160 if count >= e.batchSize { 161 e.grabAndConsumeEvents(count, eventBuf) 162 count = 0 163 } 164 case <-e.flushQueue[id]: 165 if count > 0 { 166 e.grabAndConsumeEvents(count, eventBuf) 167 count = 0 168 } 169 case <-ctx.Done(): 170 e.logger.Trace("batchWorker[", id, "]: exiting per context Done") 171 return ctx.Err() 172 } 173 } 174 } 175 176 // 177 // watchdog has a Timer that will send the results once the 178 // it has expired. 179 // 180 func (e *Events) watchdog(ctx context.Context) (err error) { 181 if e.eventTimer == nil { 182 return errors.New("invalid timer for watchdog()") 183 } 184 185 for { 186 select { 187 case <-e.eventTimer.C: 188 e.logger.Debug("Timeout expired, flushing queued events") 189 if err = e.Flush(); err != nil { 190 return 191 } 192 e.eventTimer.Reset(e.batchTimeout) 193 case <-ctx.Done(): 194 e.logger.Trace("watchdog exiting: context finished") 195 return ctx.Err() 196 } 197 } 198 } 199 200 // grabAndConsumeEvents makes a copy of the event handles, 201 // and asynchronously writes those events in its own goroutine. 202 func (e *Events) grabAndConsumeEvents(count int, eventBuf [][]byte) { 203 saved := make([][]byte, count) 204 for i := 0; i < count; i++ { 205 saved[i] = eventBuf[i] 206 eventBuf[i] = nil 207 } 208 209 go func(count int, saved [][]byte) { 210 if sendErr := e.sendEvents(saved[0:count]); sendErr != nil { 211 e.logger.Error("failed to send events") 212 } 213 }(count, saved) 214 } 215 216 func (e *Events) sendEvents(events [][]byte) error { 217 var buf bytes.Buffer 218 219 // Since we already marshalled all of the data into JSON, let's make a 220 // hand-crafted, artisanal JSON array 221 buf.WriteString("[") 222 eventCount := len(events) - 1 223 for e := range events { 224 buf.Write(events[e]) 225 if e < eventCount { 226 buf.WriteString(",") 227 } 228 } 229 buf.WriteString("]") 230 231 resp := &createEventResponse{} 232 233 _, err := e.client.Post(e.config.Region().InsightsURL(e.accountID), nil, buf.Bytes(), resp) 234 235 if err != nil { 236 return err 237 } 238 239 if !resp.Success { 240 return errors.New("failed creating custom event") 241 } 242 243 return nil 244 }