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  }