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  }