github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-linux/lib/linux.go (about)

     1  package mplinux
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  
    16  	mp "github.com/mackerelio/go-mackerel-plugin-helper"
    17  	"github.com/urfave/cli"
    18  )
    19  
    20  const (
    21  	pathVmstat = "/proc/vmstat"
    22  	pathStat   = "/proc/stat"
    23  	pathSysfs  = "/sys"
    24  )
    25  
    26  var collectVirtualDevice = regexp.MustCompile("^fio[a-z]+$") // ioDrive(FusionIO)
    27  
    28  // metric value structure
    29  // note: all metrics are add dynamic at collect*().
    30  var graphdef = map[string]mp.Graphs{}
    31  
    32  // LinuxPlugin mackerel plugin for linux
    33  type LinuxPlugin struct {
    34  	Tempfile string
    35  	Typemap  map[string]bool
    36  }
    37  
    38  // GraphDefinition interface for mackerelplugin
    39  func (c LinuxPlugin) GraphDefinition() map[string]mp.Graphs {
    40  	var err error
    41  
    42  	p := make(map[string]interface{})
    43  
    44  	if c.Typemap["all"] || c.Typemap["swap"] {
    45  		err = collectProcVmstat(pathVmstat, &p)
    46  		if err != nil {
    47  			return nil
    48  		}
    49  	}
    50  
    51  	if c.Typemap["all"] || c.Typemap["netstat"] {
    52  		err = collectNetworkStat(&p)
    53  		if err != nil {
    54  			return nil
    55  		}
    56  	}
    57  
    58  	if c.Typemap["all"] || c.Typemap["diskstats"] {
    59  		err = collectDiskStats(pathSysfs, &p)
    60  		if err != nil {
    61  			return nil
    62  		}
    63  	}
    64  
    65  	if c.Typemap["all"] || c.Typemap["proc_stat"] {
    66  		err = collectProcStat(pathStat, &p)
    67  		if err != nil {
    68  			return nil
    69  		}
    70  	}
    71  
    72  	if c.Typemap["all"] || c.Typemap["users"] {
    73  		err = collectWho(&p)
    74  		if err != nil {
    75  			return nil
    76  		}
    77  	}
    78  
    79  	return graphdef
    80  }
    81  
    82  // main function
    83  func doMain(c *cli.Context) error {
    84  	var linux LinuxPlugin
    85  
    86  	typemap := map[string]bool{}
    87  	types := c.StringSlice("type")
    88  	// If no `type` is specified, fetch all metrics
    89  	if len(types) == 0 {
    90  		typemap["all"] = true
    91  	} else {
    92  		for _, t := range types {
    93  			typemap[t] = true
    94  		}
    95  	}
    96  	linux.Typemap = typemap
    97  	helper := mp.NewMackerelPlugin(linux)
    98  	helper.Tempfile = c.String("tempfile")
    99  
   100  	helper.Run()
   101  	return nil
   102  }
   103  
   104  // FetchMetrics interface for mackerelplugin
   105  func (c LinuxPlugin) FetchMetrics() (map[string]interface{}, error) {
   106  	var err error
   107  
   108  	p := make(map[string]interface{})
   109  
   110  	if c.Typemap["all"] || c.Typemap["swap"] {
   111  		err = collectProcVmstat(pathVmstat, &p)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  
   117  	if c.Typemap["all"] || c.Typemap["netstat"] {
   118  		err = collectNetworkStat(&p)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  	}
   123  
   124  	if c.Typemap["all"] || c.Typemap["diskstats"] {
   125  		err = collectDiskStats(pathSysfs, &p)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  	}
   130  
   131  	if c.Typemap["all"] || c.Typemap["proc_stat"] {
   132  		err = collectProcStat(pathStat, &p)
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  	}
   137  
   138  	if c.Typemap["all"] || c.Typemap["users"] {
   139  		err = collectWho(&p)
   140  		if err != nil {
   141  			return nil, err
   142  		}
   143  	}
   144  
   145  	return p, nil
   146  }
   147  
   148  // collect who
   149  func collectWho(p *map[string]interface{}) error {
   150  	var err error
   151  	var data string
   152  
   153  	graphdef["linux.users"] = mp.Graphs{
   154  		Label: "Linux Users",
   155  		Unit:  "integer",
   156  		Metrics: []mp.Metrics{
   157  			{Name: "users", Label: "Users", Diff: false},
   158  		},
   159  	}
   160  
   161  	data, err = getWho()
   162  	if err != nil {
   163  		return err
   164  	}
   165  	err = parseWho(data, p)
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // parsing metrics from /proc/stat
   174  func parseWho(str string, p *map[string]interface{}) error {
   175  	str = strings.TrimSpace(str)
   176  	if str == "" {
   177  		(*p)["users"] = float64(0)
   178  		return nil
   179  	}
   180  	line := strings.Split(str, "\n")
   181  	(*p)["users"] = float64(len(line))
   182  
   183  	return nil
   184  }
   185  
   186  // Getting who
   187  func getWho() (string, error) {
   188  	cmd := exec.Command("who")
   189  	var out bytes.Buffer
   190  	cmd.Stdout = &out
   191  	err := cmd.Run()
   192  	if err != nil {
   193  		return "", err
   194  	}
   195  	return out.String(), nil
   196  }
   197  
   198  // collect /proc/stat
   199  func collectProcStat(path string, p *map[string]interface{}) error {
   200  	graphdef["linux.interrupts"] = mp.Graphs{
   201  		Label: "Linux Interrupts",
   202  		Unit:  "integer",
   203  		Metrics: []mp.Metrics{
   204  			{Name: "interrupts", Label: "Interrupts", Diff: true},
   205  		},
   206  	}
   207  	graphdef["linux.context_switches"] = mp.Graphs{
   208  		Label: "Linux Context Switches",
   209  		Unit:  "integer",
   210  		Metrics: []mp.Metrics{
   211  			{Name: "context_switches", Label: "Context Switches", Diff: true},
   212  		},
   213  	}
   214  	graphdef["linux.forks"] = mp.Graphs{
   215  		Label: "Linux Forks",
   216  		Unit:  "integer",
   217  		Metrics: []mp.Metrics{
   218  			{Name: "forks", Label: "Forks", Diff: true},
   219  		},
   220  	}
   221  
   222  	file, err := os.Open(path)
   223  	if err != nil {
   224  		return err
   225  	}
   226  	defer file.Close()
   227  	return parseProcStat(file, p)
   228  }
   229  
   230  // parsing metrics from /proc/stat
   231  func parseProcStat(r io.Reader, p *map[string]interface{}) error {
   232  	scanner := bufio.NewScanner(r)
   233  
   234  	for scanner.Scan() {
   235  		line := scanner.Text()
   236  		record := strings.Fields(line)
   237  		if len(record) < 2 {
   238  			continue
   239  		}
   240  		name := record[0]
   241  		value, errParse := atof(record[1])
   242  		if errParse != nil {
   243  			return errParse
   244  		}
   245  
   246  		switch name {
   247  		case "intr":
   248  			(*p)["interrupts"] = value
   249  		case "ctxt":
   250  			(*p)["context_switches"] = value
   251  		case "processes":
   252  			(*p)["forks"] = value
   253  		}
   254  	}
   255  
   256  	return nil
   257  }
   258  
   259  // collect /sys/block/<device>/stat
   260  // See also. http://man7.org/linux/man-pages/man5/sysfs.5.html
   261  func collectDiskStats(path string, p *map[string]interface{}) error {
   262  	var elapsedData []mp.Metrics
   263  	var rwtimeData []mp.Metrics
   264  
   265  	sysBlockDir := filepath.Join(path, "block")
   266  
   267  	devices, err := os.ReadDir(sysBlockDir)
   268  	if err != nil {
   269  		return err
   270  	}
   271  
   272  	for _, d := range devices {
   273  		fi, err := d.Info()
   274  		if err != nil {
   275  			return err
   276  		}
   277  		if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
   278  			continue
   279  		}
   280  
   281  		name := d.Name()
   282  
   283  		// /sys/block/<device> is a symbolic link for block device
   284  		realPath, err := filepath.EvalSymlinks(filepath.Join(sysBlockDir, name))
   285  		if err != nil {
   286  			return err
   287  		}
   288  
   289  		// exclude virtual device
   290  		if strings.Contains(realPath, "/devices/virtual/") {
   291  			if !collectVirtualDevice.Match([]byte(name)) {
   292  				continue
   293  			}
   294  		}
   295  
   296  		// exclude removable device
   297  		content, err := os.ReadFile(filepath.Join(realPath, "removable"))
   298  		if err != nil {
   299  			return err
   300  		}
   301  		if len(content) > 0 && string(content[0]) == "1" {
   302  			continue
   303  		}
   304  
   305  		content, err = os.ReadFile(filepath.Join(realPath, "stat"))
   306  		if err != nil {
   307  			return err
   308  		}
   309  
   310  		err = parseDiskStat(name, string(content), p)
   311  		if err != nil {
   312  			return err
   313  		}
   314  
   315  		elapsedData = append(elapsedData, mp.Metrics{Name: fmt.Sprintf("iotime_%s", name), Label: fmt.Sprintf("%s IO Time", name), Diff: true})
   316  		elapsedData = append(elapsedData, mp.Metrics{Name: fmt.Sprintf("iotime_weighted_%s", name), Label: fmt.Sprintf("%s IO Time Weighted", name), Diff: true})
   317  
   318  		rwtimeData = append(rwtimeData, mp.Metrics{Name: fmt.Sprintf("tsreading_%s", name), Label: fmt.Sprintf("%s Read", name), Diff: true})
   319  		rwtimeData = append(rwtimeData, mp.Metrics{Name: fmt.Sprintf("tswriting_%s", name), Label: fmt.Sprintf("%s Write", name), Diff: true})
   320  	}
   321  
   322  	graphdef["linux.disk.elapsed"] = mp.Graphs{
   323  		Label:   "Disk Elapsed IO Time",
   324  		Unit:    "integer",
   325  		Metrics: elapsedData,
   326  	}
   327  
   328  	graphdef["linux.disk.rwtime"] = mp.Graphs{
   329  		Label:   "Disk Read/Write Time",
   330  		Unit:    "integer",
   331  		Metrics: rwtimeData,
   332  	}
   333  
   334  	return nil
   335  }
   336  
   337  func parseDiskStat(name, stat string, p *map[string]interface{}) error {
   338  	fields := strings.Fields(stat)
   339  	if len(fields) < 11 {
   340  		return nil
   341  	}
   342  
   343  	// See also. https://www.kernel.org/doc/Documentation/block/stat.txt
   344  	(*p)[fmt.Sprintf("iotime_%s", name)], _ = atof(fields[9])           // io_ticks
   345  	(*p)[fmt.Sprintf("iotime_weighted_%s", name)], _ = atof(fields[10]) // time_in_queue
   346  	(*p)[fmt.Sprintf("tsreading_%s", name)], _ = atof(fields[3])        // read ticks
   347  	(*p)[fmt.Sprintf("tswriting_%s", name)], _ = atof(fields[7])        // write ticks
   348  
   349  	return nil
   350  }
   351  
   352  // collect ss
   353  func collectNetworkStat(p *map[string]interface{}) error {
   354  	graphdef["linux.ss"] = mp.Graphs{
   355  		Label: "Linux Network Connection States",
   356  		Unit:  "integer",
   357  		Metrics: []mp.Metrics{
   358  			{Name: "ESTAB", Label: "Established", Diff: false, Stacked: true},
   359  			{Name: "SYN-SENT", Label: "Syn Sent", Diff: false, Stacked: true},
   360  			{Name: "SYN-RECV", Label: "Syn Received", Diff: false, Stacked: true},
   361  			{Name: "FIN-WAIT-1", Label: "Fin Wait 1", Diff: false, Stacked: true},
   362  			{Name: "FIN-WAIT-2", Label: "Fin Wait 2", Diff: false, Stacked: true},
   363  			{Name: "TIME-WAIT", Label: "Time Wait", Diff: false, Stacked: true},
   364  			{Name: "UNCONN", Label: "Close", Diff: false, Stacked: true},
   365  			{Name: "CLOSE-WAIT", Label: "Close Wait", Diff: false, Stacked: true},
   366  			{Name: "LAST-ACK", Label: "Last Ack", Diff: false, Stacked: true},
   367  			{Name: "LISTEN", Label: "Listen", Diff: false, Stacked: true},
   368  			{Name: "CLOSING", Label: "Closing", Diff: false, Stacked: true},
   369  			{Name: "UNKNOWN", Label: "Unknown", Diff: false, Stacked: true},
   370  		},
   371  	}
   372  
   373  	cmd := exec.Command("ss", "-na")
   374  	out, err := cmd.StdoutPipe()
   375  	if err != nil {
   376  		return err
   377  	}
   378  	if err := cmd.Start(); err != nil {
   379  		return err
   380  	}
   381  	if err := parseSs(out, p); err != nil {
   382  		return err
   383  	}
   384  	return cmd.Wait()
   385  }
   386  
   387  // parsing metrics from ss
   388  func parseSs(r io.Reader, p *map[string]interface{}) error {
   389  	var (
   390  		status      = 0
   391  		first       = true
   392  		overstuffed = false
   393  	)
   394  	scanner := bufio.NewScanner(r)
   395  
   396  	for scanner.Scan() {
   397  		line := scanner.Text()
   398  		record := strings.Fields(line)
   399  		if len(record) < 5 {
   400  			continue
   401  		}
   402  		if first {
   403  			first = false
   404  			if record[0] == "State" {
   405  				// for RHEL6
   406  				status = 0
   407  			} else if record[1] == "State" {
   408  				// for RHEL7
   409  				status = 1
   410  			} else if record[0] == "NetidState" {
   411  				status = 1
   412  				overstuffed = true
   413  			}
   414  			continue
   415  		}
   416  		key := record[status]
   417  		if overstuffed && len(record[0]) > 5 {
   418  			key = record[0][5:]
   419  		}
   420  		v, _ := (*p)[key].(float64)
   421  		(*p)[key] = v + 1
   422  	}
   423  
   424  	return nil
   425  }
   426  
   427  // collect /proc/vmstat
   428  func collectProcVmstat(path string, p *map[string]interface{}) error {
   429  	graphdef["linux.swap"] = mp.Graphs{
   430  		Label: "Linux Swap Usage",
   431  		Unit:  "integer",
   432  		Metrics: []mp.Metrics{
   433  			{Name: "pswpin", Label: "Swap In", Diff: true},
   434  			{Name: "pswpout", Label: "Swap Out", Diff: true},
   435  		},
   436  	}
   437  
   438  	file, err := os.Open(path)
   439  	if err != nil {
   440  		return err
   441  	}
   442  	defer file.Close()
   443  	return parseProcVmstat(file, p)
   444  }
   445  
   446  // parsing metrics from /proc/vmstat
   447  func parseProcVmstat(r io.Reader, p *map[string]interface{}) error {
   448  	scanner := bufio.NewScanner(r)
   449  
   450  	for scanner.Scan() {
   451  		line := scanner.Text()
   452  		record := strings.Fields(line)
   453  		if len(record) != 2 {
   454  			continue
   455  		}
   456  		var errParse error
   457  		(*p)[record[0]], errParse = atof(record[1])
   458  		if errParse != nil {
   459  			return errParse
   460  		}
   461  	}
   462  
   463  	return nil
   464  }
   465  
   466  // atof
   467  func atof(str string) (float64, error) {
   468  	return strconv.ParseFloat(strings.Trim(str, " "), 64)
   469  }
   470  
   471  // Do the plugin
   472  func Do() {
   473  	app := cli.NewApp()
   474  	app.Name = "mackerel-plugin-linux"
   475  	app.Usage = "Get metrics from Linux."
   476  	app.Author = "Yuichiro Saito"
   477  	app.Email = "saito@heartbeats.jp"
   478  	app.Flags = flags
   479  	app.Action = doMain
   480  
   481  	err := app.Run(os.Args)
   482  	if err != nil {
   483  		log.Fatalln(err)
   484  	}
   485  }