github.com/newrelic/go-agent@v3.26.0+incompatible/internal/utilization/utilization.go (about)

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