...

Source file src/github.com/emissary-ingress/emissary/v3/pkg/metriton/reporter.go

Documentation: github.com/emissary-ingress/emissary/v3/pkg/metriton

     1  // Package metriton implements submitting telemetry data to the Metriton database.
     2  //
     3  // Metriton replaced Scout, and was originally going to have its own telemetry API and a
     4  // Scout-compatibility endpoint during the migration.  But now the Scout-compatible API is
     5  // the only thing we use.
     6  //
     7  // See also: The old scout.py package <https://pypi.org/project/scout.py/> /
     8  // <https://github.com/datawire/scout.py>.
     9  //
    10  // Things that are in scout.py, but are intentionally left of this package:
    11  //   - automatically setting the HTTP client user-agent string
    12  //   - an InstallIDFromConfigMap getter
    13  package metriton
    14  
    15  import (
    16  	"context"
    17  	"errors"
    18  	"net/http"
    19  	"net/url"
    20  	"os"
    21  	"strconv"
    22  	"strings"
    23  	"sync"
    24  )
    25  
    26  var (
    27  	// The endpoint you should use by default
    28  	DefaultEndpoint = "https://metriton.datawire.io/scout"
    29  	// Use BetaEndpoint for testing purposes without polluting production data
    30  	BetaEndpoint = "https://metriton.datawire.io/beta/scout"
    31  	// ScoutPyEndpoint is the default endpoint used by scout.py; it obeys the
    32  	// SCOUT_HOST and SCOUT_HTTPS environment variables.  I'm not sure when you should
    33  	// use it instead of DefaultEndpoint, but I'm putting it in Go so that I never
    34  	// have to look at scout.py again.
    35  	ScoutPyEndpoint = endpointFromEnv()
    36  )
    37  
    38  func getenvDefault(varname, def string) string {
    39  	ret := os.Getenv(varname)
    40  	if ret == "" {
    41  		ret = def
    42  	}
    43  	return ret
    44  }
    45  
    46  func endpointFromEnv() string {
    47  	host := getenvDefault("SCOUT_HOST", "metriton.datawire.io")
    48  	useHTTPS, _ := strconv.ParseBool(getenvDefault("SCOUT_HTTPS", "1"))
    49  	ret := &url.URL{
    50  		Scheme: "http",
    51  		Host:   host,
    52  		Path:   "/scout",
    53  	}
    54  	if useHTTPS {
    55  		ret.Scheme = "https"
    56  	}
    57  	return ret.String()
    58  }
    59  
    60  // Reporter is a client to
    61  type Reporter struct {
    62  	// Information about the application submitting telemetry.
    63  	Application string
    64  	Version     string
    65  	// GetInstallID is a function, instead of a fixed 'InstallID' string, in order to
    66  	// facilitate getting it lazily; and possibly updating the BaseMetadata based on
    67  	// the journey to getting the install ID.  See StaticInstallID and
    68  	// InstallIDFromFilesystem.
    69  	GetInstallID func(*Reporter) (string, error)
    70  	// BaseMetadata will be merged in to the data passed to each call to .Report().
    71  	// If the data passed to .Report() and BaseMetadata have a key in common, the
    72  	// value passed as an argument to .Report() wins.
    73  	BaseMetadata map[string]interface{}
    74  
    75  	// The HTTP client used to to submit the request; if this is nil, then
    76  	// http.DefaultClient is used.
    77  	Client *http.Client
    78  	// The endpoint URL to submit to; if this is empty, then DefaultEndpoint is used.
    79  	Endpoint string
    80  
    81  	mu          sync.Mutex
    82  	initialized bool
    83  	disabled    bool
    84  	installID   string
    85  }
    86  
    87  func (r *Reporter) ensureInitialized() error {
    88  	if r.Application == "" {
    89  		return errors.New("Reporter.Application may not be empty")
    90  	}
    91  	if r.Version == "" {
    92  		return errors.New("Reporter.Version may not be empty")
    93  	}
    94  	if r.GetInstallID == nil {
    95  		return errors.New("Reporter.GetInstallID may not be nil")
    96  	}
    97  
    98  	if r.initialized {
    99  		return nil
   100  	}
   101  
   102  	if r.BaseMetadata == nil {
   103  		r.BaseMetadata = make(map[string]interface{})
   104  	}
   105  
   106  	r.disabled = IsDisabledByUser()
   107  
   108  	installID, err := r.GetInstallID(r)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	r.installID = installID
   113  
   114  	r.initialized = true
   115  
   116  	return nil
   117  }
   118  
   119  // IsDisabledByUser returns whether telemetry reporting is disabled by the user.
   120  func IsDisabledByUser() bool {
   121  	// From scout.py
   122  	if strings.HasPrefix(os.Getenv("TRAVIS_REPO_SLUG"), "datawire/") {
   123  		return true
   124  	}
   125  
   126  	// This mimics the existing ad-hoc Go clients, rather than scout.py; it is a more
   127  	// sensitive trigger than scout.py's __is_disabled() (which only accepts "1",
   128  	// "true", "yes"; case-insensitive).
   129  	return os.Getenv("SCOUT_DISABLE") != ""
   130  }
   131  
   132  func (r *Reporter) InstallID() string {
   133  	r.mu.Lock()
   134  	defer r.mu.Unlock()
   135  	_ = r.ensureInitialized()
   136  	return r.installID
   137  }
   138  
   139  // Report submits a telemetry report to Metriton.  It is safe to call .Report() from
   140  // different goroutines.  It is NOT safe to mutate the public fields in the Reporter while
   141  // .Report() is being called.
   142  func (r *Reporter) Report(ctx context.Context, metadata map[string]interface{}) (*Response, error) {
   143  	r.mu.Lock()
   144  
   145  	if err := r.ensureInitialized(); err != nil {
   146  		r.mu.Unlock()
   147  		return nil, err
   148  	}
   149  
   150  	var resp *Response
   151  	var err error
   152  
   153  	if r.disabled {
   154  		r.mu.Unlock()
   155  	} else {
   156  		client := r.Client
   157  		if client == nil {
   158  			client = http.DefaultClient
   159  		}
   160  
   161  		endpoint := r.Endpoint
   162  		if endpoint == "" {
   163  			endpoint = DefaultEndpoint
   164  		}
   165  
   166  		mergedMetadata := make(map[string]interface{}, len(r.BaseMetadata)+len(metadata))
   167  		// FWIW, the resolution of conflicts between 'r.BaseMetadata' and 'metadata'
   168  		// mimics scout.py; I'm not sure whether that aspect of scout.py's API is
   169  		// intentional or incidental.
   170  		for k, v := range r.BaseMetadata {
   171  			mergedMetadata[k] = v
   172  		}
   173  		for k, v := range metadata {
   174  			mergedMetadata[k] = v
   175  		}
   176  
   177  		report := Report{
   178  			Application: r.Application,
   179  			InstallID:   r.installID,
   180  			Version:     r.Version,
   181  			Metadata:    mergedMetadata,
   182  		}
   183  
   184  		r.mu.Unlock()
   185  		resp, err = report.Send(ctx, client, endpoint)
   186  		if err != nil {
   187  			return nil, err
   188  		}
   189  	}
   190  
   191  	if resp == nil {
   192  		// This mimics scout.py
   193  		resp = &Response{
   194  			AppInfo: AppInfo{
   195  				LatestVersion: r.Version,
   196  			},
   197  		}
   198  	}
   199  
   200  	if resp != nil && resp.DisableScout {
   201  		r.mu.Lock()
   202  		r.disabled = true
   203  		r.mu.Unlock()
   204  	}
   205  
   206  	return resp, nil
   207  }
   208  

View as plain text