github.com/lulzWill/go-agent@v2.1.2+incompatible/internal/utilization/utilization.go (about)

     1  // Package utilization implements the Utilization spec, available at
     2  // https://source.datanerd.us/agents/agent-specs/blob/master/Utilization.md
     3  //
     4  package utilization
     5  
     6  import (
     7  	"net/http"
     8  	"runtime"
     9  	"sync"
    10  
    11  	"github.com/lulzWill/go-agent/internal/logger"
    12  	"github.com/lulzWill/go-agent/internal/sysinfo"
    13  )
    14  
    15  const (
    16  	metadataVersion = 3
    17  )
    18  
    19  // Config controls the behavior of utilization information capture.
    20  type Config struct {
    21  	DetectAWS         bool
    22  	DetectAzure       bool
    23  	DetectGCP         bool
    24  	DetectPCF         bool
    25  	DetectDocker      bool
    26  	LogicalProcessors int
    27  	TotalRAMMIB       int
    28  	BillingHostname   string
    29  }
    30  
    31  type override struct {
    32  	LogicalProcessors *int   `json:"logical_processors,omitempty"`
    33  	TotalRAMMIB       *int   `json:"total_ram_mib,omitempty"`
    34  	BillingHostname   string `json:"hostname,omitempty"`
    35  }
    36  
    37  // Data contains utilization system information.
    38  type Data struct {
    39  	MetadataVersion int `json:"metadata_version"`
    40  	// Although `runtime.NumCPU()` will never fail, this field is a pointer
    41  	// to facilitate the cross agent tests.
    42  	LogicalProcessors *int      `json:"logical_processors"`
    43  	RAMMiB            *uint64   `json:"total_ram_mib"`
    44  	Hostname          string    `json:"hostname"`
    45  	BootID            string    `json:"boot_id,omitempty"`
    46  	Vendors           *vendors  `json:"vendors,omitempty"`
    47  	Config            *override `json:"config,omitempty"`
    48  }
    49  
    50  var (
    51  	sampleRAMMib    = uint64(1024)
    52  	sampleLogicProc = int(16)
    53  	// SampleData contains sample utilization data useful for testing.
    54  	SampleData = Data{
    55  		MetadataVersion:   metadataVersion,
    56  		LogicalProcessors: &sampleLogicProc,
    57  		RAMMiB:            &sampleRAMMib,
    58  		Hostname:          "my-hostname",
    59  	}
    60  )
    61  
    62  type docker struct {
    63  	ID string `json:"id,omitempty"`
    64  }
    65  
    66  type vendors struct {
    67  	AWS    *aws    `json:"aws,omitempty"`
    68  	Azure  *azure  `json:"azure,omitempty"`
    69  	GCP    *gcp    `json:"gcp,omitempty"`
    70  	PCF    *pcf    `json:"pcf,omitempty"`
    71  	Docker *docker `json:"docker,omitempty"`
    72  }
    73  
    74  func (v *vendors) isEmpty() bool {
    75  	return v.AWS == nil && v.Azure == nil && v.GCP == nil && v.PCF == nil && v.Docker == nil
    76  }
    77  
    78  func overrideFromConfig(config Config) *override {
    79  	ov := &override{}
    80  
    81  	if 0 != config.LogicalProcessors {
    82  		x := config.LogicalProcessors
    83  		ov.LogicalProcessors = &x
    84  	}
    85  	if 0 != config.TotalRAMMIB {
    86  		x := config.TotalRAMMIB
    87  		ov.TotalRAMMIB = &x
    88  	}
    89  	ov.BillingHostname = config.BillingHostname
    90  
    91  	if "" == ov.BillingHostname &&
    92  		nil == ov.LogicalProcessors &&
    93  		nil == ov.TotalRAMMIB {
    94  		ov = nil
    95  	}
    96  	return ov
    97  }
    98  
    99  // Gather gathers system utilization data.
   100  func Gather(config Config, lg logger.Logger) *Data {
   101  	client := &http.Client{
   102  		Timeout: providerTimeout,
   103  	}
   104  	return gatherWithClient(config, lg, client)
   105  }
   106  
   107  func gatherWithClient(config Config, lg logger.Logger, client *http.Client) *Data {
   108  	var wg sync.WaitGroup
   109  
   110  	cpu := runtime.NumCPU()
   111  	uDat := &Data{
   112  		MetadataVersion:   metadataVersion,
   113  		LogicalProcessors: &cpu,
   114  		Vendors:           &vendors{},
   115  	}
   116  
   117  	warnGatherError := func(datatype string, err error) {
   118  		lg.Debug("error gathering utilization data", map[string]interface{}{
   119  			"error":    err.Error(),
   120  			"datatype": datatype,
   121  		})
   122  	}
   123  
   124  	// This closure allows us to run each gather function in a separate goroutine
   125  	// and wait for them at the end by closing over the wg WaitGroup we
   126  	// instantiated at the start of the function.
   127  	goGather := func(datatype string, gather func(*Data, *http.Client) error) {
   128  		wg.Add(1)
   129  		go func() {
   130  			// Note that locking around util is not neccesary since
   131  			// WaitGroup provides acts as a memory barrier:
   132  			// https://groups.google.com/d/msg/golang-nuts/5oHzhzXCcmM/utEwIAApCQAJ
   133  			// Thus this code is fine as long as each routine is
   134  			// modifying a different field of util.
   135  			defer wg.Done()
   136  			if err := gather(uDat, client); err != nil {
   137  				warnGatherError(datatype, err)
   138  			}
   139  		}()
   140  	}
   141  
   142  	// Kick off gathering which requires network calls in goroutines.
   143  
   144  	if config.DetectAWS {
   145  		goGather("aws", gatherAWS)
   146  	}
   147  
   148  	if config.DetectAzure {
   149  		goGather("azure", gatherAzure)
   150  	}
   151  
   152  	if config.DetectPCF {
   153  		goGather("pcf", gatherPCF)
   154  	}
   155  
   156  	if config.DetectGCP {
   157  		goGather("gcp", gatherGCP)
   158  	}
   159  
   160  	// Do non-network gathering sequentially since it is fast.
   161  
   162  	if id, err := sysinfo.BootID(); err != nil {
   163  		if err != sysinfo.ErrFeatureUnsupported {
   164  			warnGatherError("bootid", err)
   165  		}
   166  	} else {
   167  		uDat.BootID = id
   168  	}
   169  
   170  	if config.DetectDocker {
   171  		if id, err := sysinfo.DockerID(); err != nil {
   172  			if err != sysinfo.ErrFeatureUnsupported &&
   173  				err != sysinfo.ErrDockerNotFound {
   174  				warnGatherError("docker", err)
   175  			}
   176  		} else {
   177  			uDat.Vendors.Docker = &docker{ID: id}
   178  		}
   179  	}
   180  
   181  	if hostname, err := sysinfo.Hostname(); nil == err {
   182  		uDat.Hostname = hostname
   183  	} else {
   184  		warnGatherError("hostname", err)
   185  	}
   186  
   187  	if bts, err := sysinfo.PhysicalMemoryBytes(); nil == err {
   188  		mib := sysinfo.BytesToMebibytes(bts)
   189  		uDat.RAMMiB = &mib
   190  	} else {
   191  		warnGatherError("memory", err)
   192  	}
   193  
   194  	// Now we wait for everything!
   195  	wg.Wait()
   196  
   197  	// Override whatever needs to be overridden.
   198  	uDat.Config = overrideFromConfig(config)
   199  
   200  	if uDat.Vendors.isEmpty() {
   201  		// Per spec, we MUST NOT send any vendors hash if it's empty.
   202  		uDat.Vendors = nil
   203  	}
   204  
   205  	return uDat
   206  }