github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak/src/client/internal/monitoring/resource_usage_fetcher_unix.go (about) 1 // Copyright 2017 Google Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build linux 16 17 // Package monitoring contains utilities for gathering data about resource usage in 18 // order to monitor client-side resource usage. 19 package monitoring 20 21 import ( 22 "fmt" 23 "os" 24 "os/exec" 25 "strconv" 26 "strings" 27 "time" 28 29 log "github.com/golang/glog" 30 ) 31 32 // TODO: Support monitoring on other platforms. 33 34 // ResourceUsage contains resource-usage data for a single process. 35 type ResourceUsage struct { 36 // When the resource-usage data was retrieved. 37 Timestamp time.Time 38 39 // Amount of CPU time scheduled for a process so far in user mode. 40 UserCPUMillis float64 41 42 // Amount of CPU time scheduled for a process so far in kernel mode. 43 SystemCPUMillis float64 44 45 // Resident set size for a process. 46 ResidentMemory int64 47 48 // Number of open file descriptors. 49 NumFDs int32 50 } 51 52 // ResourceUsageFetcher obtains resource-usage data for a process from the OS. 53 type ResourceUsageFetcher struct{} 54 55 // ResourceUsageFromFinishedCmd returns a ResourceUsage struct with 56 // information from exec.Cmd.ProcessState. NOTE that this is only possible 57 // after the process has finished and has been waited for, and will most 58 // probably panic otherwise. 59 // This function doesn't fill in ResourceUsage.ResidentMemory. 60 func (f ResourceUsageFetcher) ResourceUsageFromFinishedCmd(cmd *exec.Cmd) *ResourceUsage { 61 return &ResourceUsage{ 62 Timestamp: time.Now(), 63 UserCPUMillis: float64(cmd.ProcessState.UserTime().Nanoseconds()) / 1e6, 64 SystemCPUMillis: float64(cmd.ProcessState.SystemTime().Nanoseconds()) / 1e6, 65 } 66 } 67 68 // In principle, pageSize and ticksPerSecond should be found through the system 69 // calls C.sysconf(C._SC_PAGE_SIZE), C.sysconf(C._SC_CLK_TCK). However, it seems 70 // to be fixed for all linux platforms that go supports so we hardcode it to 71 // avoid requiring cgo. 72 const ( 73 // 4 KiB 74 pageSize = 4096 75 ) 76 77 func getNumFDs(pid int) int32 { 78 fdPath := fmt.Sprintf("/proc/%d/fd", pid) 79 files, err := os.ReadDir(fdPath) 80 if err != nil { 81 log.Errorf("can't list the file descriptors folder: %v", err) 82 return 0 83 } 84 85 return int32(len(files)) 86 } 87 88 // ResourceUsageForPID returns a ResourceUsage struct with information 89 // from /proc/<PID>/stat and /proc/<PID>/statm . This is only possible with running processes, 90 // an error will be returned if the corresponding procfs entry is not present. 91 func (f ResourceUsageFetcher) ResourceUsageForPID(pid int) (*ResourceUsage, error) { 92 timestamp := time.Now() 93 statFilename := fmt.Sprintf("/proc/%d/stat", pid) 94 stat, err := os.ReadFile(statFilename) 95 if err != nil { 96 return nil, fmt.Errorf("error while reading %s: %v", statFilename, err) 97 } 98 99 splitStat := strings.Split(string(stat), " ") 100 101 const expectedStatLen = 17 102 if len(splitStat) < expectedStatLen { 103 return nil, fmt.Errorf("unexpected format in %s - expected at least %d fields", statFilename, expectedStatLen) 104 } 105 106 utime, err := strconv.ParseInt(splitStat[13], 10, 64) 107 if err != nil { 108 return nil, fmt.Errorf("error while parsing utime from %s: %v", statFilename, err) 109 } 110 111 stime, err := strconv.ParseInt(splitStat[14], 10, 64) 112 if err != nil { 113 return nil, fmt.Errorf("error while parsing stime from %s: %v", statFilename, err) 114 } 115 116 cutime, err := strconv.ParseInt(splitStat[15], 10, 64) 117 if err != nil { 118 return nil, fmt.Errorf("error while parsing cutime from %s: %v", statFilename, err) 119 } 120 121 cstime, err := strconv.ParseInt(splitStat[16], 10, 64) 122 if err != nil { 123 return nil, fmt.Errorf("error while parsing cstime from %s: %v", statFilename, err) 124 } 125 126 statmFilename := fmt.Sprintf("/proc/%d/statm", pid) 127 statm, err := os.ReadFile(statmFilename) 128 if err != nil { 129 return nil, fmt.Errorf("error while reading %s: %v", statmFilename, err) 130 } 131 132 splitStatm := strings.Split(string(statm), " ") 133 134 const expectedStatmLen = 2 135 if len(splitStatm) < expectedStatmLen { 136 return nil, fmt.Errorf("unexpected format in %s - expected at least %d fields", statmFilename, expectedStatmLen) 137 } 138 139 resident, err := strconv.ParseInt(splitStatm[1], 10, 64) 140 if err != nil { 141 return nil, fmt.Errorf("error while parsing resident from %s: %v", statmFilename, err) 142 } 143 144 return &ResourceUsage{ 145 Timestamp: timestamp, 146 UserCPUMillis: float64((utime + cutime) * 10), // Assume rate of 100 ticks/second 147 SystemCPUMillis: float64((stime + cstime) * 10), // Assume rate of 100 ticks/second 148 ResidentMemory: resident * pageSize, 149 NumFDs: getNumFDs(pid), 150 }, nil 151 } 152 153 // DebugStatusForPID returns a string containing extra debug info about resource-usage that may not be 154 // captured in ResourceUsage. This is only possible for running processes. 155 func (f ResourceUsageFetcher) DebugStatusForPID(pid int) (string, error) { 156 statusFilename := fmt.Sprintf("/proc/%d/status", pid) 157 status, err := os.ReadFile(statusFilename) 158 if err != nil { 159 return "", fmt.Errorf("error while reading %s: %v", statusFilename, err) 160 } 161 return string(status), nil 162 }