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  }