github.com/lulzWill/go-agent@v2.1.2+incompatible/internal_app.go (about) 1 package newrelic 2 3 import ( 4 "errors" 5 "fmt" 6 "math" 7 "net/http" 8 "os" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/lulzWill/go-agent/internal" 14 "github.com/lulzWill/go-agent/internal/logger" 15 ) 16 17 var ( 18 // NEW_RELIC_DEBUG_LOGGING can be set to anything to enable additional 19 // debug logging: the agent will log every transaction's data at info 20 // level. 21 envDebugLogging = "NEW_RELIC_DEBUG_LOGGING" 22 debugLogging = os.Getenv(envDebugLogging) 23 ) 24 25 type dataConsumer interface { 26 Consume(internal.AgentRunID, internal.Harvestable) 27 } 28 29 type appData struct { 30 id internal.AgentRunID 31 data internal.Harvestable 32 } 33 34 type app struct { 35 config Config 36 rpmControls internal.RpmControls 37 testHarvest *internal.Harvest 38 39 // placeholderRun is used when the application is not connected. 40 placeholderRun *appRun 41 42 // initiateShutdown is used to tell the processor to shutdown. 43 initiateShutdown chan struct{} 44 45 // shutdownStarted and shutdownComplete are closed by the processor 46 // goroutine to indicate the shutdown status. Two channels are used so 47 // that the call of app.Shutdown() can block until shutdown has 48 // completed but other goroutines can exit when shutdown has started. 49 // This is not just an optimization: This prevents a deadlock if 50 // harvesting data during the shutdown fails and an attempt is made to 51 // merge the data into the next harvest. 52 shutdownStarted chan struct{} 53 shutdownComplete chan struct{} 54 55 // Sends to these channels should not occur without a <-shutdownStarted 56 // select option to prevent deadlock. 57 dataChan chan appData 58 collectorErrorChan chan error 59 connectChan chan *appRun 60 61 harvestTicker *time.Ticker 62 63 // This mutex protects both `run` and `err`, both of which should only 64 // be accessed using getState and setState. 65 sync.RWMutex 66 // run is non-nil when the app is successfully connected. It is 67 // immutable. 68 run *appRun 69 // err is non-nil if the application will never be connected again 70 // (disconnect, license exception, shutdown). 71 err error 72 73 flushStart chan bool 74 flushComplete chan bool 75 } 76 77 // appRun contains information regarding a single connection session with the 78 // collector. It is immutable after creation at application connect. 79 type appRun struct { 80 *internal.ConnectReply 81 82 // AttributeConfig is calculated on every connect since it depends on 83 // the security policies. 84 AttributeConfig *internal.AttributeConfig 85 } 86 87 func newAppRun(config Config, reply *internal.ConnectReply) *appRun { 88 return &appRun{ 89 ConnectReply: reply, 90 AttributeConfig: internal.CreateAttributeConfig(internal.AttributeConfigInput{ 91 Attributes: convertAttributeDestinationConfig(config.Attributes), 92 ErrorCollector: convertAttributeDestinationConfig(config.ErrorCollector.Attributes), 93 TransactionEvents: convertAttributeDestinationConfig(config.TransactionEvents.Attributes), 94 TransactionTracer: convertAttributeDestinationConfig(config.TransactionTracer.Attributes), 95 }, reply.SecurityPolicies.AttributesInclude.Enabled()), 96 } 97 } 98 99 func isFatalHarvestError(e error) bool { 100 return internal.IsDisconnect(e) || 101 internal.IsLicenseException(e) || 102 internal.IsRestartException(e) 103 } 104 105 func shouldSaveFailedHarvest(e error) bool { 106 if e == internal.ErrPayloadTooLarge || e == internal.ErrUnsupportedMedia { 107 return false 108 } 109 return true 110 } 111 112 func (app *app) doHarvest(h *internal.Harvest, harvestStart time.Time, run *appRun) { 113 h.CreateFinalMetrics() 114 h.Metrics = h.Metrics.ApplyRules(run.MetricRules) 115 116 payloads := h.Payloads() 117 for cmd, p := range payloads { 118 119 data, err := p.Data(run.RunID.String(), harvestStart) 120 121 if nil == data && nil == err { 122 continue 123 } 124 125 if nil == err { 126 call := internal.RpmCmd{ 127 Collector: run.Collector, 128 RunID: run.RunID.String(), 129 Name: cmd, 130 Data: data, 131 } 132 133 // The reply from harvest calls is always unused. 134 _, err = internal.CollectorRequest(call, app.rpmControls) 135 } 136 137 if nil == err { 138 continue 139 } 140 141 if isFatalHarvestError(err) { 142 select { 143 case app.collectorErrorChan <- err: 144 case <-app.shutdownStarted: 145 } 146 return 147 } 148 149 app.config.Logger.Warn("harvest failure", map[string]interface{}{ 150 "cmd": cmd, 151 "error": err.Error(), 152 }) 153 154 if shouldSaveFailedHarvest(err) { 155 app.Consume(run.RunID, p) 156 } 157 } 158 app.flushComplete <- true 159 } 160 161 func connectAttempt(app *app) (*appRun, error) { 162 reply, err := internal.ConnectAttempt(config{app.config}, app.config.SecurityPoliciesToken, app.rpmControls) 163 if nil != err { 164 return nil, err 165 } 166 return newAppRun(app.config, reply), nil 167 } 168 169 func (app *app) connectRoutine() { 170 for { 171 run, err := connectAttempt(app) 172 if nil == err { 173 select { 174 case app.connectChan <- run: 175 case <-app.shutdownStarted: 176 } 177 return 178 } 179 180 if internal.IsDisconnect(err) || internal.IsLicenseException(err) { 181 select { 182 case app.collectorErrorChan <- err: 183 case <-app.shutdownStarted: 184 } 185 return 186 } 187 188 app.config.Logger.Warn("application connect failure", map[string]interface{}{ 189 "error": err.Error(), 190 }) 191 192 time.Sleep(internal.ConnectBackoff) 193 } 194 } 195 196 func debug(data internal.Harvestable, lg Logger) { 197 now := time.Now() 198 h := internal.NewHarvest(now) 199 data.MergeIntoHarvest(h) 200 ps := h.Payloads() 201 for cmd, p := range ps { 202 d, err := p.Data("agent run id", now) 203 if nil == d && nil == err { 204 continue 205 } 206 if nil != err { 207 lg.Info("integration", map[string]interface{}{ 208 "cmd": cmd, 209 "error": err.Error(), 210 }) 211 continue 212 } 213 lg.Info("integration", map[string]interface{}{ 214 "cmd": cmd, 215 "data": internal.JSONString(d), 216 }) 217 } 218 } 219 220 func processConnectMessages(run *appRun, lg Logger) { 221 for _, msg := range run.Messages { 222 event := "collector message" 223 cn := map[string]interface{}{"msg": msg.Message} 224 225 switch strings.ToLower(msg.Level) { 226 case "error": 227 lg.Error(event, cn) 228 case "warn": 229 lg.Warn(event, cn) 230 case "info": 231 lg.Info(event, cn) 232 case "debug", "verbose": 233 lg.Debug(event, cn) 234 } 235 } 236 } 237 238 func (app *app) process() { 239 // Both the harvest and the run are non-nil when the app is connected, 240 // and nil otherwise. 241 var h *internal.Harvest 242 var run *appRun 243 244 for { 245 select { 246 case f := <-app.flushStart: 247 if f && nil != run { 248 now := time.Now() 249 go app.doHarvest(h, now, run) 250 h = internal.NewHarvest(now) 251 } 252 case <-app.harvestTicker.C: 253 if nil != run { 254 now := time.Now() 255 go app.doHarvest(h, now, run) 256 h = internal.NewHarvest(now) 257 } 258 case d := <-app.dataChan: 259 if nil != run && run.RunID == d.id { 260 d.data.MergeIntoHarvest(h) 261 } 262 case <-app.initiateShutdown: 263 close(app.shutdownStarted) 264 265 // Remove the run before merging any final data to 266 // ensure a bounded number of receives from dataChan. 267 app.setState(nil, errors.New("application shut down")) 268 app.harvestTicker.Stop() 269 270 if nil != run { 271 for done := false; !done; { 272 select { 273 case d := <-app.dataChan: 274 if run.RunID == d.id { 275 d.data.MergeIntoHarvest(h) 276 } 277 default: 278 done = true 279 } 280 } 281 app.doHarvest(h, time.Now(), run) 282 } 283 284 close(app.shutdownComplete) 285 return 286 case err := <-app.collectorErrorChan: 287 run = nil 288 h = nil 289 app.setState(nil, nil) 290 291 switch { 292 case internal.IsDisconnect(err): 293 app.setState(nil, err) 294 app.config.Logger.Error("application disconnected", map[string]interface{}{ 295 "app": app.config.AppName, 296 "err": err.Error(), 297 }) 298 case internal.IsLicenseException(err): 299 app.setState(nil, err) 300 app.config.Logger.Error("invalid license", map[string]interface{}{ 301 "app": app.config.AppName, 302 "license": app.config.License, 303 }) 304 case internal.IsRestartException(err): 305 app.config.Logger.Info("application restarted", map[string]interface{}{ 306 "app": app.config.AppName, 307 }) 308 go app.connectRoutine() 309 } 310 case run = <-app.connectChan: 311 h = internal.NewHarvest(time.Now()) 312 app.setState(run, nil) 313 314 app.config.Logger.Info("application connected", map[string]interface{}{ 315 "app": app.config.AppName, 316 "run": run.RunID.String(), 317 }) 318 processConnectMessages(run, app.config.Logger) 319 } 320 } 321 } 322 323 func (app *app) Flush() { 324 app.flushStart <- true 325 <-app.flushComplete 326 } 327 328 func (app *app) Shutdown(timeout time.Duration) { 329 if !app.config.Enabled { 330 return 331 } 332 333 select { 334 case app.initiateShutdown <- struct{}{}: 335 default: 336 } 337 338 // Block until shutdown is done or timeout occurs. 339 t := time.NewTimer(timeout) 340 select { 341 case <-app.shutdownComplete: 342 case <-t.C: 343 } 344 t.Stop() 345 346 app.config.Logger.Info("application shutdown", map[string]interface{}{ 347 "app": app.config.AppName, 348 }) 349 } 350 351 func convertAttributeDestinationConfig(c AttributeDestinationConfig) internal.AttributeDestinationConfig { 352 return internal.AttributeDestinationConfig{ 353 Enabled: c.Enabled, 354 Include: c.Include, 355 Exclude: c.Exclude, 356 } 357 } 358 359 func runSampler(app *app, period time.Duration) { 360 previous := internal.GetSample(time.Now(), app.config.Logger) 361 t := time.NewTicker(period) 362 for { 363 select { 364 case now := <-t.C: 365 current := internal.GetSample(now, app.config.Logger) 366 run, _ := app.getState() 367 app.Consume(run.RunID, internal.GetStats(internal.Samples{ 368 Previous: previous, 369 Current: current, 370 })) 371 previous = current 372 case <-app.shutdownStarted: 373 t.Stop() 374 return 375 } 376 } 377 } 378 379 func (app *app) WaitForConnection(timeout time.Duration) error { 380 if !app.config.Enabled { 381 return nil 382 } 383 deadline := time.Now().Add(timeout) 384 pollPeriod := 50 * time.Millisecond 385 386 for { 387 run, err := app.getState() 388 if nil != err { 389 return err 390 } 391 if run.RunID != "" { 392 return nil 393 } 394 if time.Now().After(deadline) { 395 return fmt.Errorf("timeout out after %s", timeout.String()) 396 } 397 time.Sleep(pollPeriod) 398 } 399 } 400 401 func newApp(c Config) (Application, error) { 402 c = copyConfigReferenceFields(c) 403 if err := c.Validate(); nil != err { 404 return nil, err 405 } 406 if nil == c.Logger { 407 c.Logger = logger.ShimLogger{} 408 } 409 app := &app{ 410 config: c, 411 412 placeholderRun: newAppRun(c, internal.ConnectReplyDefaults()), 413 414 // This channel must be buffered since Shutdown makes a 415 // non-blocking send attempt. 416 initiateShutdown: make(chan struct{}, 1), 417 418 shutdownStarted: make(chan struct{}), 419 shutdownComplete: make(chan struct{}), 420 connectChan: make(chan *appRun, 1), 421 collectorErrorChan: make(chan error, 1), 422 dataChan: make(chan appData, internal.AppDataChanSize), 423 rpmControls: internal.RpmControls{ 424 License: c.License, 425 Client: &http.Client{ 426 Transport: c.Transport, 427 Timeout: internal.CollectorTimeout, 428 }, 429 Logger: c.Logger, 430 AgentVersion: Version, 431 }, 432 flushStart: make(chan bool, 1), 433 flushComplete: make(chan bool, 1), 434 } 435 436 app.config.Logger.Info("application created", map[string]interface{}{ 437 "app": app.config.AppName, 438 "version": Version, 439 "enabled": app.config.Enabled, 440 }) 441 442 if !app.config.Enabled { 443 return app, nil 444 } 445 446 app.harvestTicker = time.NewTicker(internal.HarvestPeriod) 447 448 go app.process() 449 go app.connectRoutine() 450 451 if app.config.RuntimeSampler.Enabled { 452 go runSampler(app, internal.RuntimeSamplerPeriod) 453 } 454 455 return app, nil 456 } 457 458 type expectApp interface { 459 internal.Expect 460 Application 461 } 462 463 func newTestApp(replyfn func(*internal.ConnectReply), cfg Config) (expectApp, error) { 464 cfg.Enabled = false 465 application, err := newApp(cfg) 466 if nil != err { 467 return nil, err 468 } 469 app := application.(*app) 470 if nil != replyfn { 471 replyfn(app.placeholderRun.ConnectReply) 472 app.placeholderRun = newAppRun(cfg, app.placeholderRun.ConnectReply) 473 } 474 app.testHarvest = internal.NewHarvest(time.Now()) 475 476 return app, nil 477 } 478 479 func (app *app) getState() (*appRun, error) { 480 app.RLock() 481 defer app.RUnlock() 482 483 run := app.run 484 if nil == run { 485 run = app.placeholderRun 486 } 487 return run, app.err 488 } 489 490 func (app *app) setState(run *appRun, err error) { 491 app.Lock() 492 defer app.Unlock() 493 494 app.run = run 495 app.err = err 496 } 497 498 // StartTransaction implements newrelic.Application's StartTransaction. 499 func (app *app) StartTransaction(name string, w http.ResponseWriter, r *http.Request) Transaction { 500 run, _ := app.getState() 501 return upgradeTxn(newTxn(txnInput{ 502 Config: app.config, 503 Reply: run.ConnectReply, 504 W: w, 505 Consumer: app, 506 attrConfig: run.AttributeConfig, 507 }, r, name)) 508 } 509 510 var ( 511 errHighSecurityEnabled = errors.New("high security enabled") 512 errCustomEventsDisabled = errors.New("custom events disabled") 513 errCustomEventsRemoteDisabled = errors.New("custom events disabled by server") 514 ) 515 516 // RecordCustomEvent implements newrelic.Application's RecordCustomEvent. 517 func (app *app) RecordCustomEvent(eventType string, params map[string]interface{}) error { 518 if app.config.HighSecurity { 519 return errHighSecurityEnabled 520 } 521 522 if !app.config.CustomInsightsEvents.Enabled { 523 return errCustomEventsDisabled 524 } 525 526 event, e := internal.CreateCustomEvent(eventType, params, time.Now()) 527 if nil != e { 528 return e 529 } 530 531 run, _ := app.getState() 532 if !run.CollectCustomEvents { 533 return errCustomEventsRemoteDisabled 534 } 535 536 if !run.SecurityPolicies.CustomEvents.Enabled() { 537 return errSecurityPolicy 538 } 539 540 app.Consume(run.RunID, event) 541 542 return nil 543 } 544 545 var ( 546 errMetricInf = errors.New("invalid metric value: inf") 547 errMetricNaN = errors.New("invalid metric value: NaN") 548 errMetricNameEmpty = errors.New("missing metric name") 549 ) 550 551 // RecordCustomMetric implements newrelic.Application's RecordCustomMetric. 552 func (app *app) RecordCustomMetric(name string, value float64) error { 553 if math.IsNaN(value) { 554 return errMetricNaN 555 } 556 if math.IsInf(value, 0) { 557 return errMetricInf 558 } 559 if "" == name { 560 return errMetricNameEmpty 561 } 562 run, _ := app.getState() 563 app.Consume(run.RunID, internal.CustomMetric{ 564 RawInputName: name, 565 Value: value, 566 }) 567 return nil 568 } 569 570 func (app *app) Consume(id internal.AgentRunID, data internal.Harvestable) { 571 if "" != debugLogging { 572 debug(data, app.config.Logger) 573 } 574 575 if nil != app.testHarvest { 576 data.MergeIntoHarvest(app.testHarvest) 577 return 578 } 579 580 if "" == id { 581 return 582 } 583 584 select { 585 case app.dataChan <- appData{id, data}: 586 case <-app.shutdownStarted: 587 } 588 } 589 590 func (app *app) ExpectCustomEvents(t internal.Validator, want []internal.WantEvent) { 591 internal.ExpectCustomEvents(internal.ExtendValidator(t, "custom events"), app.testHarvest.CustomEvents, want) 592 } 593 594 func (app *app) ExpectErrors(t internal.Validator, want []internal.WantError) { 595 t = internal.ExtendValidator(t, "traced errors") 596 internal.ExpectErrors(t, app.testHarvest.ErrorTraces, want) 597 } 598 599 func (app *app) ExpectErrorEvents(t internal.Validator, want []internal.WantEvent) { 600 t = internal.ExtendValidator(t, "error events") 601 internal.ExpectErrorEvents(t, app.testHarvest.ErrorEvents, want) 602 } 603 604 func (app *app) ExpectTxnEvents(t internal.Validator, want []internal.WantEvent) { 605 t = internal.ExtendValidator(t, "txn events") 606 internal.ExpectTxnEvents(t, app.testHarvest.TxnEvents, want) 607 } 608 609 func (app *app) ExpectMetrics(t internal.Validator, want []internal.WantMetric) { 610 t = internal.ExtendValidator(t, "metrics") 611 internal.ExpectMetrics(t, app.testHarvest.Metrics, want) 612 } 613 614 func (app *app) ExpectTxnTraces(t internal.Validator, want []internal.WantTxnTrace) { 615 t = internal.ExtendValidator(t, "txn traces") 616 internal.ExpectTxnTraces(t, app.testHarvest.TxnTraces, want) 617 } 618 619 func (app *app) ExpectSlowQueries(t internal.Validator, want []internal.WantSlowQuery) { 620 t = internal.ExtendValidator(t, "slow queries") 621 internal.ExpectSlowQueries(t, app.testHarvest.SlowSQLs, want) 622 }