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 }