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 }