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 }