github.com/instana/go-sensor@v1.62.2-0.20240520081010-4919868049e1/process/stats_reader_linux.go (about) 1 // (c) Copyright IBM Corp. 2021 2 // (c) Copyright Instana Inc. 2020 3 4 //go:build linux 5 // +build linux 6 7 package process 8 9 import ( 10 "bufio" 11 "fmt" 12 "io/ioutil" 13 "os" 14 "path" 15 "strings" 16 ) 17 18 const ( 19 pageSize = 4 << 10 // standard setting, applicable for most systems 20 procPath = "/proc" 21 ) 22 23 type statsReader struct { 24 ProcPath string 25 Command string 26 } 27 28 // Stats returns a process resource stats reader for current process 29 func Stats() statsReader { 30 return statsReader{ 31 ProcPath: procPath, 32 Command: path.Base(os.Args[0]), 33 } 34 } 35 36 // Memory returns memory stats for current process 37 func (rdr statsReader) Memory() (MemStats, error) { 38 fd, err := os.Open(rdr.ProcPath + "/self/statm") 39 if err != nil { 40 return MemStats{}, nil 41 } 42 defer fd.Close() 43 44 var total, rss, shared int 45 46 // The fields come in order described in `/proc/[pid]/statm` section 47 // of https://man7.org/linux/man-pages/man5/proc.5.html 48 if _, err := fmt.Fscanf(fd, "%d %d %d", 49 &total, // size 50 &rss, // resident 51 &shared, // shared 52 // ... the rest of the fields are not used and thus omitted 53 ); err != nil { 54 return MemStats{}, fmt.Errorf("failed to parse %s: %s", fd.Name(), err) 55 } 56 57 return MemStats{ 58 Total: total * pageSize, 59 Rss: rss * pageSize, 60 Shared: shared * pageSize, 61 }, nil 62 } 63 64 // CPU returns CPU stats for current process and the CPU tick they were taken on 65 func (rdr statsReader) CPU() (CPUStats, int, error) { 66 fd, err := os.Open(rdr.ProcPath + "/self/stat") 67 if err != nil { 68 return CPUStats{}, 0, nil 69 } 70 defer fd.Close() 71 72 var ( 73 stats CPUStats 74 skipInt int 75 skipCh byte 76 ) 77 78 // The command in `/proc/self/stat` output is truncated to 15 bytes (16 including the terminating null byte) 79 comm := rdr.Command 80 if len(comm) > 15 { 81 comm = comm[:15] 82 } 83 84 // The fields come in order described in `/proc/[pid]/stat` section 85 // of https://man7.org/linux/man-pages/man5/proc.5.html. We skip parsing 86 // the `comm` field since it may contain space characters that break fmt.Fscanf format. 87 if _, err := fmt.Fscanf(fd, "%d ("+comm+") %c %d %d %d %d %d %d %d %d %d %d %d %d", 88 &skipInt, // pid 89 &skipCh, // state 90 &skipInt, // ppid 91 &skipInt, // pgrp 92 &skipInt, // session 93 &skipInt, // tty_nr 94 &skipInt, // tpgid 95 &skipInt, // flags 96 &skipInt, // minflt 97 &skipInt, // cminflt 98 &skipInt, // majflt 99 &skipInt, // cmajflt 100 &stats.User, // utime 101 &stats.System, // stime 102 // ... the rest of the fields are not used and thus omitted 103 ); err != nil { 104 return stats, 0, fmt.Errorf("failed to parse %s: %s", fd.Name(), err) 105 } 106 107 tick, err := rdr.currentTick() 108 if err != nil { 109 return stats, 0, fmt.Errorf("failed to get current CPU tick: %s", err) 110 } 111 112 return stats, tick, nil 113 } 114 115 // currentTick parses /proc/stat, sums up the total number of ticks spent on each CPU and averages them 116 // by the number of CPUs 117 func (rdr statsReader) currentTick() (int, error) { 118 fd, err := os.Open(rdr.ProcPath + "/stat") 119 if err != nil { 120 return 0, nil 121 } 122 defer fd.Close() 123 124 sc := bufio.NewScanner(fd) 125 sc.Split(bufio.ScanLines) 126 127 var ( 128 ticks, cpuCount int 129 user, nice, sys, idle, iowait, irq, softIRQ, steal int 130 skipStr string 131 ) 132 133 for sc.Scan() { 134 s := sc.Text() 135 if !strings.HasPrefix(s, "cpu") { 136 continue 137 } 138 139 if strings.HasPrefix(s, "cpu ") { // skip total CPU line 140 continue 141 } 142 143 // The fields come in order described in `/proc/stat` section 144 // of https://man7.org/linux/man-pages/man5/proc.5.html 145 if _, err := fmt.Sscanf(s, "%s %d %d %d %d %d %d %d %d", 146 &skipStr, // CPU label 147 &user, 148 &nice, 149 &sys, 150 &idle, 151 &iowait, 152 &irq, 153 &softIRQ, 154 &steal, 155 // ... the rest of the fields are not used and thus omitted 156 ); err != nil { 157 return 0, fmt.Errorf("failed to parse %s: %s", fd.Name(), err) 158 } 159 160 ticks += user + nice + sys + idle + iowait + irq + softIRQ + steal 161 cpuCount++ 162 } 163 164 if err := sc.Err(); err != nil { 165 return 0, fmt.Errorf("failed to read %s: %s", fd.Name(), err) 166 } 167 168 if cpuCount < 2 { 169 return ticks, nil 170 } 171 172 return ticks / cpuCount, nil 173 } 174 175 // Limits returns resource limits configured for current process 176 func (rdr statsReader) Limits() (ResourceLimits, error) { 177 fd, err := os.Open(rdr.ProcPath + "/self/limits") 178 if err != nil { 179 return ResourceLimits{}, nil 180 } 181 defer fd.Close() 182 183 sc := bufio.NewScanner(fd) 184 sc.Split(bufio.ScanLines) 185 186 var limits ResourceLimits 187 188 for sc.Scan() { 189 s := sc.Text() 190 if !strings.HasPrefix(s, "Max open files") { 191 continue 192 } 193 194 s = strings.TrimLeft(s[14:], " \t") // trim the "max open files" prefix along with trailing space 195 if !strings.HasPrefix(s, "unlimited") { 196 if _, err := fmt.Sscanf(s, "%d", &limits.OpenFiles.Max); err != nil { 197 return limits, fmt.Errorf("unexpected %s format: %s", fd.Name(), err) 198 } 199 } 200 201 break 202 } 203 204 if err := sc.Err(); err != nil { 205 return limits, fmt.Errorf("failed to read %s: %s", fd.Name(), err) 206 } 207 208 fdNum, err := rdr.currentOpenFiles() 209 if err != nil { 210 return limits, fmt.Errorf("failed to get the number of open files: %s", err) 211 } 212 213 limits.OpenFiles.Current = fdNum 214 215 return limits, nil 216 } 217 218 func (rdr statsReader) currentOpenFiles() (int, error) { 219 fds, err := ioutil.ReadDir(rdr.ProcPath + "/self/fd/") 220 if err != nil { 221 return 0, fmt.Errorf("failed to list %s: %s", rdr.ProcPath+"/fd/", err) 222 } 223 224 return len(fds), nil 225 }