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  }