github.com/ubuntu/ubuntu-report@v1.7.4-0.20240410144652-96f37d845fac/internal/metrics/metrics.go (about) 1 package metrics 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "math" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 "strings" 14 15 "github.com/pkg/errors" 16 log "github.com/sirupsen/logrus" 17 "github.com/ubuntu/ubuntu-report/internal/utils" 18 ) 19 20 const ( 21 installerLogsPath = "var/log/installer/telemetry" 22 upgradeLogsPath = "var/log/upgrade/telemetry" 23 ) 24 25 // Metrics collect system, upgrade and installer data 26 type Metrics struct { 27 root string 28 screenInfoCmd *exec.Cmd 29 spaceInfoCmd *exec.Cmd 30 cpuInfoCmd *exec.Cmd 31 gpuInfoCmd *exec.Cmd 32 archCmd *exec.Cmd 33 libc6Cmd *exec.Cmd 34 hwCapCmd *exec.Cmd 35 getenv GetenvFn 36 } 37 38 // New return a new metrics element with optional testing functions 39 func New(options ...func(*Metrics) error) (Metrics, error) { 40 41 hwCapCmd := getHwCapCmd(options) 42 43 m := Metrics{ 44 root: "/", 45 screenInfoCmd: setCommand("xrandr"), 46 spaceInfoCmd: setCommand("df"), 47 cpuInfoCmd: setCommand("lscpu", "-J"), 48 gpuInfoCmd: setCommand("lspci", "-n"), 49 archCmd: setCommand("dpkg", "--print-architecture"), 50 hwCapCmd: hwCapCmd, 51 getenv: os.Getenv, 52 } 53 m.cpuInfoCmd.Env = []string{"LANG=C"} 54 55 for _, options := range options { 56 if err := options(&m); err != nil { 57 return m, err 58 } 59 } 60 61 return m, nil 62 } 63 64 // GetIDS returns distro and version information 65 func (m Metrics) GetIDS() (string, string, error) { 66 p := filepath.Join(m.root, "etc", "os-release") 67 f, err := os.Open(p) 68 if err != nil { 69 return "", "", errors.Wrapf(err, "couldn't open %s", p) 70 } 71 defer f.Close() 72 73 scanner := bufio.NewScanner(f) 74 dRe := regexp.MustCompile(`^ID=(.*)$`) 75 vRe := regexp.MustCompile(`^VERSION_ID="(.*)"$`) 76 var distro, version string 77 for scanner.Scan() { 78 v := dRe.FindStringSubmatch(scanner.Text()) 79 if v != nil { 80 distro = strings.TrimSpace(v[1]) 81 } 82 v = vRe.FindStringSubmatch(scanner.Text()) 83 if v != nil { 84 version = strings.TrimSpace(v[1]) 85 } 86 } 87 88 if err := scanner.Err(); (distro == "" || version == "") && err != nil { 89 return "", "", errors.Wrap(err, "error while scanning") 90 } 91 92 if distro == "" || version == "" { 93 return "", "", errors.Errorf("distribution '%s' or version '%s' information missing", distro, version) 94 } 95 96 return distro, version, nil 97 } 98 99 func setCommand(cmds ...string) *exec.Cmd { 100 if len(cmds) == 1 { 101 return exec.Command(cmds[0]) 102 } 103 return exec.Command(cmds[0], cmds[1:]...) 104 } 105 106 // Collect system, installer and update info, returning a json formatted byte 107 func (m Metrics) Collect() ([]byte, error) { 108 log.Debugf("Collecting metrics on system with root set to %s", m.root) 109 r := metrics{} 110 111 r.Version = m.getVersion() 112 113 if vendor, product, family, dcd := m.getOEM(); vendor != "" || product != "" { 114 r.OEM = &struct { 115 Vendor string 116 Product string 117 Family string 118 DCD string `json:",omitempty"` 119 }{vendor, product, family, dcd} 120 } 121 if vendor, version := m.getBIOS(); vendor != "" || version != "" { 122 r.BIOS = &struct { 123 Vendor string 124 Version string 125 }{vendor, version} 126 } 127 128 cpu := m.getCPU() 129 if cpu != (cpuInfo{}) { 130 r.CPU = &cpu 131 } else { 132 r.CPU = nil 133 } 134 r.Arch = m.getArch() 135 r.GPU = m.getGPU() 136 r.RAM = m.getRAM() 137 r.Disks = m.getDisks() 138 r.Partitions = m.getPartitions() 139 r.Screens = m.getScreens() 140 r.HwCap = m.getHwCap() 141 142 a := m.getAutologin() 143 r.Autologin = &a 144 l := m.getLivePatch() 145 r.LivePatch = &l 146 147 de := m.getenv("XDG_CURRENT_DESKTOP") 148 sessionName := m.getenv("XDG_SESSION_DESKTOP") 149 sessionType := m.getenv("XDG_SESSION_TYPE") 150 if de != "" || sessionName != "" || sessionType != "" { 151 r.Session = &struct { 152 DE string 153 Name string 154 Type string 155 }{de, sessionName, sessionType} 156 } 157 r.Language = m.getLanguage() 158 r.Timezone = m.getTimeZone() 159 160 r.Install = m.installerInfo() 161 r.Upgrade = m.upgradeInfo() 162 163 d, err := json.Marshal(r) 164 return d, errors.Wrapf(err, "can't be converted to a valid json") 165 } 166 167 func (m Metrics) getLanguage() string { 168 lang := m.getenv("LC_ALL") 169 if lang == "" { 170 lang = m.getenv("LANG") 171 } 172 if lang == "" { 173 lang = strings.Split(m.getenv("LANGUAGE"), ":")[0] 174 } 175 return strings.Split(lang, ".")[0] 176 } 177 178 func convKBToGB(s string) (float64, error) { 179 v, err := strconv.Atoi(s) 180 if err != nil { 181 return 0, errors.Wrapf(err, "couldn't convert %s to an integer", s) 182 } 183 // convert in GB (SI) and round it to 0.1 184 f := float64(v) / (1000 * 1000) 185 return math.Round(f*10) / 10, nil 186 } 187 188 func getHwCapCmd(options []func(*Metrics) error) *exec.Cmd { 189 // set up the map for architecture -> ld binary 190 ldPath := make(map[string]string, 3) 191 ldPath["amd64"] = "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" 192 ldPath["ppc64el"] = "/lib/powerpc64le-linux-gnu/ld64.so.2" 193 ldPath["s390x"] = "/lib/s390x-linux-gnu/ld64.so.1" 194 195 // check if libc6Cmd has been mocked 196 mTemp := Metrics{} 197 for _, mockFuncs := range options { 198 mockFuncs(&mTemp) 199 } 200 var libc6Cmd *exec.Cmd 201 if mTemp.libc6Cmd != nil { 202 libc6Cmd = mTemp.libc6Cmd 203 } else { 204 libc6Cmd = setCommand("dpkg", "--status", "libc6") 205 } 206 207 // Make sure we have glibc version > 2.33 208 r := runCmd(libc6Cmd) 209 libc6Result, err := filterFirst(r, `^(?:Version: (.*))`, false) 210 if err != nil { 211 log.Infof("Couldn't get glibc version: "+utils.ErrFormat, err) 212 return nil 213 } 214 if strings.Compare(libc6Result, "2.33") < 0 { 215 // glibc versions older than 2.33 cannot report hwcap 216 return nil 217 } 218 219 // find the architecture so we can directly assign hwCapCmd 220 archCmd := setCommand("dpkg", "--print-architecture") 221 r = runCmd(archCmd) 222 buf := new(bytes.Buffer) 223 buf.ReadFrom(r) 224 arch := strings.TrimSpace(buf.String()) 225 226 if _, found := ldPath[arch]; found { 227 return setCommand(ldPath[arch], "--help") 228 } else { 229 // architecture has no supported hwcap, string will be empty 230 return nil 231 } 232 }