go.undefinedlabs.com/scopeagent@v0.4.2/agent/agent.go (about)

     1  package agent
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net/url"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"sync"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/google/uuid"
    19  	"github.com/mitchellh/go-homedir"
    20  	"github.com/opentracing/opentracing-go"
    21  
    22  	"go.undefinedlabs.com/scopeagent/env"
    23  	scopeError "go.undefinedlabs.com/scopeagent/errors"
    24  	"go.undefinedlabs.com/scopeagent/instrumentation"
    25  	scopetesting "go.undefinedlabs.com/scopeagent/instrumentation/testing"
    26  	"go.undefinedlabs.com/scopeagent/reflection"
    27  	"go.undefinedlabs.com/scopeagent/runner"
    28  	"go.undefinedlabs.com/scopeagent/tags"
    29  	"go.undefinedlabs.com/scopeagent/tracer"
    30  )
    31  
    32  type (
    33  	Agent struct {
    34  		tracer opentracing.Tracer
    35  
    36  		apiEndpoint string
    37  		apiKey      string
    38  
    39  		agentId          string
    40  		version          string
    41  		metadata         map[string]interface{}
    42  		debugMode        bool
    43  		testingMode      bool
    44  		setGlobalTracer  bool
    45  		panicAsFail      bool
    46  		failRetriesCount int
    47  
    48  		recorder         *SpanRecorder
    49  		recorderFilename string
    50  		flushFrequency   time.Duration
    51  
    52  		optionalRecorders []tracer.SpanRecorder
    53  
    54  		userAgent string
    55  		agentType string
    56  
    57  		logger          *log.Logger
    58  		printReportOnce sync.Once
    59  
    60  		cache *localCache
    61  	}
    62  
    63  	Option func(*Agent)
    64  )
    65  
    66  var (
    67  	version = "0.4.2"
    68  
    69  	testingModeFrequency    = time.Second
    70  	nonTestingModeFrequency = time.Minute
    71  )
    72  
    73  func WithApiKey(apiKey string) Option {
    74  	return func(agent *Agent) {
    75  		agent.apiKey = apiKey
    76  	}
    77  }
    78  
    79  func WithApiEndpoint(apiEndpoint string) Option {
    80  	return func(agent *Agent) {
    81  		agent.apiEndpoint = apiEndpoint
    82  	}
    83  }
    84  
    85  func WithServiceName(service string) Option {
    86  	return func(agent *Agent) {
    87  		agent.metadata[tags.Service] = service
    88  	}
    89  }
    90  
    91  func WithDebugEnabled() Option {
    92  	return func(agent *Agent) {
    93  		agent.debugMode = true
    94  	}
    95  }
    96  
    97  func WithTestingModeEnabled() Option {
    98  	return func(agent *Agent) {
    99  		agent.testingMode = true
   100  	}
   101  }
   102  
   103  func WithSetGlobalTracer() Option {
   104  	return func(agent *Agent) {
   105  		agent.setGlobalTracer = true
   106  	}
   107  }
   108  
   109  func WithMetadata(values map[string]interface{}) Option {
   110  	return func(agent *Agent) {
   111  		for k, v := range values {
   112  			agent.metadata[k] = v
   113  		}
   114  	}
   115  }
   116  
   117  func WithGitInfo(repository string, commitSha string, sourceRoot string) Option {
   118  	return func(agent *Agent) {
   119  		agent.metadata[tags.Repository] = repository
   120  		agent.metadata[tags.Commit] = commitSha
   121  		agent.metadata[tags.SourceRoot] = sourceRoot
   122  	}
   123  }
   124  
   125  func WithUserAgent(userAgent string) Option {
   126  	return func(agent *Agent) {
   127  		userAgent = strings.TrimSpace(userAgent)
   128  		if userAgent != "" {
   129  			agent.userAgent = userAgent
   130  		}
   131  	}
   132  }
   133  
   134  func WithAgentType(agentType string) Option {
   135  	return func(agent *Agent) {
   136  		agentType = strings.TrimSpace(agentType)
   137  		if agentType != "" {
   138  			agent.agentType = agentType
   139  		}
   140  	}
   141  }
   142  
   143  func WithConfigurationKeys(keys []string) Option {
   144  	return func(agent *Agent) {
   145  		if keys != nil && len(keys) > 0 {
   146  			agent.metadata[tags.ConfigurationKeys] = keys
   147  		}
   148  	}
   149  }
   150  
   151  func WithConfiguration(values map[string]interface{}) Option {
   152  	return func(agent *Agent) {
   153  		if values == nil {
   154  			return
   155  		}
   156  		var keys []string
   157  		for k, v := range values {
   158  			agent.metadata[k] = v
   159  			keys = append(keys, k)
   160  		}
   161  		agent.metadata[tags.ConfigurationKeys] = keys
   162  	}
   163  }
   164  
   165  func WithRetriesOnFail(retriesCount int) Option {
   166  	return func(agent *Agent) {
   167  		agent.failRetriesCount = retriesCount
   168  	}
   169  }
   170  
   171  func WithHandlePanicAsFail() Option {
   172  	return func(agent *Agent) {
   173  		agent.panicAsFail = true
   174  	}
   175  }
   176  
   177  func WithRecorders(recorders ...tracer.SpanRecorder) Option {
   178  	return func(agent *Agent) {
   179  		agent.optionalRecorders = recorders
   180  	}
   181  }
   182  
   183  func WithGlobalPanicHandler() Option {
   184  	return func(agent *Agent) {
   185  		reflection.AddPanicHandler(func(e interface{}) {
   186  			instrumentation.Logger().Printf("Panic handler triggered by: %v.\nFlushing agent, sending partial results...", scopeError.GetCurrentError(e).ErrorStack())
   187  			agent.Flush()
   188  		})
   189  		reflection.AddOnPanicExitHandler(func(e interface{}) {
   190  			instrumentation.Logger().Printf("Process is going to end by: %v,\nStopping agent...", scopeError.GetCurrentError(e).ErrorStack())
   191  			scopetesting.PanicAllRunningTests(e, 3)
   192  			agent.Stop()
   193  		})
   194  	}
   195  }
   196  
   197  // Creates a new Scope Agent instance
   198  func NewAgent(options ...Option) (*Agent, error) {
   199  	agent := new(Agent)
   200  	agent.metadata = make(map[string]interface{})
   201  	agent.version = version
   202  	agent.agentId = generateAgentID()
   203  	agent.userAgent = fmt.Sprintf("scope-agent-go/%s", agent.version)
   204  	agent.panicAsFail = false
   205  	agent.failRetriesCount = 0
   206  
   207  	for _, opt := range options {
   208  		opt(agent)
   209  	}
   210  
   211  	if err := agent.setupLogging(); err != nil {
   212  		agent.logger = log.New(ioutil.Discard, "", 0)
   213  	}
   214  
   215  	agent.debugMode = agent.debugMode || env.ScopeDebug.Value
   216  
   217  	configProfile := GetConfigCurrentProfile()
   218  
   219  	if agent.apiKey == "" || agent.apiEndpoint == "" {
   220  		if dsn, set := env.ScopeDsn.Tuple(); set && dsn != "" {
   221  			dsnApiKey, dsnApiEndpoint, dsnErr := parseDSN(dsn)
   222  			if dsnErr != nil {
   223  				agent.logger.Printf("Error parsing dsn value: %v\n", dsnErr)
   224  			} else {
   225  				agent.apiKey = dsnApiKey
   226  				agent.apiEndpoint = dsnApiEndpoint
   227  			}
   228  		} else {
   229  			agent.logger.Println("environment variable $SCOPE_DSN not found")
   230  		}
   231  	}
   232  
   233  	if agent.apiKey == "" {
   234  		if apiKey, set := env.ScopeApiKey.Tuple(); set && apiKey != "" {
   235  			agent.apiKey = apiKey
   236  		} else if configProfile != nil {
   237  			agent.logger.Println("API key found in the native app configuration")
   238  			agent.apiKey = configProfile.ApiKey
   239  		} else {
   240  			agent.logger.Println("API key not found, agent can't be started")
   241  			return nil, errors.New("Scope DSN not found. Tests will run but no results will be reported to Scope. More info at https://docs.scope.dev/")
   242  		}
   243  	}
   244  
   245  	if agent.apiEndpoint == "" {
   246  		if endpoint, set := env.ScopeApiEndpoint.Tuple(); set && endpoint != "" {
   247  			agent.apiEndpoint = endpoint
   248  		} else if configProfile != nil {
   249  			agent.logger.Println("API endpoint found in the native app configuration")
   250  			agent.apiEndpoint = configProfile.ApiEndpoint
   251  		} else {
   252  			agent.logger.Printf("using default endpoint: %v\n", endpoint)
   253  			agent.apiEndpoint = endpoint
   254  		}
   255  	}
   256  
   257  	// Agent data
   258  	if agent.agentType == "" {
   259  		agent.agentType = "go"
   260  	}
   261  	agent.metadata[tags.AgentID] = agent.agentId
   262  	agent.metadata[tags.AgentVersion] = version
   263  	agent.metadata[tags.AgentType] = agent.agentType
   264  	agent.metadata[tags.TestingMode] = agent.testingMode
   265  
   266  	// Platform data
   267  	agent.metadata[tags.PlatformName] = runtime.GOOS
   268  	agent.metadata[tags.PlatformArchitecture] = runtime.GOARCH
   269  	if runtime.GOARCH == "amd64" {
   270  		agent.metadata[tags.ProcessArchitecture] = "X64"
   271  	} else if runtime.GOARCH == "386" {
   272  		agent.metadata[tags.ProcessArchitecture] = "X86"
   273  	} else if runtime.GOARCH == "arm" {
   274  		agent.metadata[tags.ProcessArchitecture] = "Arm"
   275  	} else if runtime.GOARCH == "arm64" {
   276  		agent.metadata[tags.ProcessArchitecture] = "Arm64"
   277  	}
   278  
   279  	// Current folder
   280  	wd, _ := os.Getwd()
   281  	agent.metadata[tags.CurrentFolder] = filepath.Clean(wd)
   282  
   283  	// Hostname
   284  	hostname, _ := os.Hostname()
   285  	agent.metadata[tags.Hostname] = hostname
   286  
   287  	// Go version
   288  	agent.metadata[tags.GoVersion] = runtime.Version()
   289  
   290  	// Service name
   291  	addElementToMapIfEmpty(agent.metadata, tags.Service, env.ScopeService.Value)
   292  
   293  	// Configurations
   294  	addElementToMapIfEmpty(agent.metadata, tags.ConfigurationKeys, env.ScopeConfiguration.Value)
   295  
   296  	// Metadata
   297  	addToMapIfEmpty(agent.metadata, env.ScopeMetadata.Value)
   298  
   299  	// Git data
   300  	addToMapIfEmpty(agent.metadata, getGitInfoFromEnv())
   301  	addToMapIfEmpty(agent.metadata, getCIMetadata())
   302  	addToMapIfEmpty(agent.metadata, getGitInfoFromGitFolder())
   303  
   304  	agent.metadata[tags.Diff] = getGitDiff()
   305  
   306  	agent.metadata[tags.InContainer] = isRunningInContainer()
   307  
   308  	// Dependencies
   309  	agent.metadata[tags.Dependencies] = getDependencyMap()
   310  
   311  	// Expand '~' in source root
   312  	var sourceRoot string
   313  	if sRoot, ok := agent.metadata[tags.SourceRoot]; ok {
   314  		cSRoot := sRoot.(string)
   315  		cSRoot = filepath.Clean(cSRoot)
   316  		if sRootEx, err := homedir.Expand(cSRoot); err == nil {
   317  			cSRoot = sRootEx
   318  		}
   319  		sourceRoot = cSRoot
   320  	}
   321  	if sourceRoot == "" {
   322  		sourceRoot = getGoModDir()
   323  	}
   324  	agent.metadata[tags.SourceRoot] = sourceRoot
   325  
   326  	// Capabilities
   327  	capabilities := map[string]interface{}{
   328  		tags.Capabilities_CodePath:      testing.CoverMode() != "",
   329  		tags.Capabilities_RunnerCache:   false,
   330  		tags.Capabilities_RunnerRetries: agent.failRetriesCount > 0,
   331  	}
   332  	agent.metadata[tags.Capabilities] = capabilities
   333  
   334  	enableRemoteConfig := false
   335  	if env.ScopeRunnerEnabled.Value {
   336  		// runner is enabled
   337  		capabilities[tags.Capabilities_RunnerCache] = true
   338  		if env.ScopeRunnerIncludeBranches.Value == nil && env.ScopeRunnerExcludeBranches.Value == nil {
   339  			// both include and exclude branches are not defined
   340  			enableRemoteConfig = true
   341  		} else if iBranch, ok := agent.metadata[tags.Branch]; ok {
   342  			branch := iBranch.(string)
   343  			included := sliceContains(env.ScopeRunnerIncludeBranches.Value, branch)
   344  			excluded := sliceContains(env.ScopeRunnerExcludeBranches.Value, branch)
   345  			enableRemoteConfig = included // By default we use the value inside the include slice
   346  			if env.ScopeRunnerExcludeBranches.Value != nil {
   347  				if included && excluded {
   348  					// If appears in both slices, write in the logger and disable the runner configuration
   349  					agent.logger.Printf("The branch '%v' appears in both included and excluded branches. The branch will be excluded.", branch)
   350  					enableRemoteConfig = false
   351  				} else {
   352  					// We enable the remote config if is include or not excluded
   353  					enableRemoteConfig = included || !excluded
   354  				}
   355  			}
   356  		}
   357  	}
   358  
   359  	if !agent.testingMode {
   360  		if env.ScopeTestingMode.IsSet {
   361  			agent.testingMode = env.ScopeTestingMode.Value
   362  		} else {
   363  			agent.testingMode = agent.metadata[tags.CI].(bool)
   364  		}
   365  	}
   366  
   367  	if agent.failRetriesCount == 0 {
   368  		agent.failRetriesCount = env.ScopeTestingFailRetries.Value
   369  	}
   370  	agent.panicAsFail = agent.panicAsFail || env.ScopeTestingPanicAsFail.Value
   371  
   372  	agent.flushFrequency = nonTestingModeFrequency
   373  	if agent.testingMode {
   374  		agent.flushFrequency = testingModeFrequency
   375  	}
   376  
   377  	if agent.debugMode {
   378  		agent.logMetadata()
   379  	}
   380  
   381  	//
   382  	agent.cache = newLocalCache(agent.getRemoteConfigRequest(), cacheTimeout, agent.debugMode, agent.logger)
   383  
   384  	agent.recorder = NewSpanRecorder(agent)
   385  	var recorder tracer.SpanRecorder = agent.recorder
   386  	if agent.optionalRecorders != nil {
   387  		recorders := append(agent.optionalRecorders, agent.recorder)
   388  		recorder = tracer.NewMultiRecorder(recorders...)
   389  	}
   390  
   391  	agent.tracer = tracer.NewWithOptions(tracer.Options{
   392  		Recorder: recorder,
   393  		ShouldSample: func(traceID uuid.UUID) bool {
   394  			return true
   395  		},
   396  		MaxLogsPerSpan: 10000,
   397  		// Log the error in the current span
   398  		OnSpanFinishPanic: scopeError.WriteExceptionEventInRawSpan,
   399  	})
   400  	instrumentation.SetTracer(agent.tracer)
   401  	instrumentation.SetLogger(agent.logger)
   402  	instrumentation.SetSourceRoot(sourceRoot)
   403  	if enableRemoteConfig {
   404  		instrumentation.SetRemoteConfiguration(agent.loadRemoteConfiguration())
   405  	}
   406  	if agent.setGlobalTracer || env.ScopeTracerGlobal.Value {
   407  		opentracing.SetGlobalTracer(agent.Tracer())
   408  	}
   409  
   410  	return agent, nil
   411  }
   412  
   413  func getGoModDir() string {
   414  	dir, err := os.Getwd()
   415  	if err != nil {
   416  		return filepath.Dir("/")
   417  	}
   418  	for {
   419  		rel, _ := filepath.Rel("/", dir)
   420  		// Exit the loop once we reach the basePath.
   421  		if rel == "." {
   422  			return filepath.Dir("/")
   423  		}
   424  		modPath := fmt.Sprintf("%v/go.mod", dir)
   425  		if _, err := os.Stat(modPath); err == nil {
   426  			return dir
   427  		}
   428  		// Going up!
   429  		dir += "/.."
   430  	}
   431  }
   432  
   433  func (a *Agent) setupLogging() error {
   434  	filename := fmt.Sprintf("scope-go-%s-%s.log", time.Now().Format("20060102150405"), a.agentId)
   435  	dir, err := getLogPath()
   436  	if err != nil {
   437  		return err
   438  	}
   439  	a.recorderFilename = filepath.Join(dir, filename)
   440  
   441  	file, err := os.OpenFile(a.recorderFilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
   442  	if err != nil {
   443  		return err
   444  	}
   445  
   446  	a.logger = log.New(file, "", log.LstdFlags|log.Lshortfile)
   447  	return nil
   448  }
   449  
   450  func (a *Agent) Tracer() opentracing.Tracer {
   451  	return a.tracer
   452  }
   453  
   454  func (a *Agent) Logger() *log.Logger {
   455  	return a.logger
   456  }
   457  
   458  // Runs the test suite
   459  func (a *Agent) Run(m *testing.M) int {
   460  	defer a.Stop()
   461  	return runner.Run(m, runner.Options{
   462  		FailRetries: a.failRetriesCount,
   463  		PanicAsFail: a.panicAsFail,
   464  		Logger:      a.logger,
   465  		OnPanic: func(t *testing.T, err interface{}) {
   466  			if t != nil {
   467  				a.logger.Printf("test '%s' has panicked (%v), stopping agent", t.Name(), err)
   468  			} else {
   469  				a.logger.Printf("panic: %v", err)
   470  			}
   471  			a.Stop()
   472  		},
   473  	})
   474  }
   475  
   476  // Stops the agent
   477  func (a *Agent) Stop() {
   478  	a.logger.Println("Scope agent is stopping gracefully...")
   479  	if a.recorder != nil {
   480  		a.recorder.Stop()
   481  	}
   482  	a.PrintReport()
   483  }
   484  
   485  // Flush agent buffer
   486  func (a *Agent) Flush() {
   487  	a.logger.Println("Flushing agent buffer...")
   488  	if a.recorder != nil {
   489  		if err := a.recorder.Flush(); err != nil {
   490  			a.logger.Println(err)
   491  		}
   492  	}
   493  }
   494  
   495  func generateAgentID() string {
   496  	agentId, err := uuid.NewRandom()
   497  	if err != nil {
   498  		panic(err)
   499  	}
   500  	return agentId.String()
   501  }
   502  
   503  func getLogPath() (string, error) {
   504  	if env.ScopeLoggerRoot.IsSet {
   505  		return env.ScopeLoggerRoot.Value, nil
   506  	}
   507  
   508  	logFolder := ""
   509  	if runtime.GOOS == "linux" {
   510  		logFolder = "/var/log/scope"
   511  	} else {
   512  		homeDir, err := homedir.Dir()
   513  		if err != nil {
   514  			return "", err
   515  		}
   516  		if runtime.GOOS == "windows" {
   517  			logFolder = fmt.Sprintf("%s/AppData/Roaming/scope/logs", homeDir)
   518  		} else if runtime.GOOS == "darwin" {
   519  			logFolder = fmt.Sprintf("%s/Library/Logs/Scope", homeDir)
   520  		}
   521  	}
   522  
   523  	if logFolder != "" {
   524  		if _, err := os.Stat(logFolder); err == nil {
   525  			return logFolder, nil
   526  		} else if os.IsNotExist(err) && os.Mkdir(logFolder, 0755) == nil {
   527  			return logFolder, nil
   528  		}
   529  	}
   530  
   531  	// If the log folder can't be used we return a temporal path, so we don't miss the agent logs
   532  	logFolder = filepath.Join(os.TempDir(), "scope")
   533  	if _, err := os.Stat(logFolder); err == nil {
   534  		return logFolder, nil
   535  	} else if os.IsNotExist(err) && os.Mkdir(logFolder, 0755) == nil {
   536  		return logFolder, nil
   537  	} else {
   538  		return "", err
   539  	}
   540  }
   541  
   542  func parseDSN(dsnString string) (apiKey string, apiEndpoint string, err error) {
   543  	uri, err := url.Parse(dsnString)
   544  	if err != nil {
   545  		return "", "", err
   546  	}
   547  	if uri.User != nil {
   548  		apiKey = uri.User.Username()
   549  	}
   550  	uri.User = nil
   551  	apiEndpoint = uri.String()
   552  	return
   553  }
   554  
   555  func (a *Agent) getUrl(pathValue string) string {
   556  	uri, err := url.Parse(a.apiEndpoint)
   557  	if err != nil {
   558  		a.logger.Fatal(err)
   559  	}
   560  	uri.Path = path.Join(uri.Path, pathValue)
   561  	return uri.String()
   562  }