github.com/mier85/go-sensor@v1.30.1-0.20220920111756-9bf41b3bc7e0/process/stats_reader_linux.go (about)

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