Skip to content

Commit

Permalink
rrd analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
divolgin committed Nov 3, 2020
1 parent 734f8c3 commit 8ee4a09
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ For details on creating the custom resource files that drive support-bundle coll

For questions about using Troubleshoot, there's a [Replicated Community](https://help.replicated.com/community) forum, and a [#app-troubleshoot channel in Kubernetes Slack](https://kubernetes.slack.com/channels/app-troubleshoot).

# Building

The following packages are required for building the project from source code:

pkg-config
librrd-dev
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ require (
github.com/stretchr/testify v1.5.1
github.com/tj/go-spin v1.1.0
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/ziutek/rrd v0.0.3
go.opencensus.io v0.22.0 // indirect
go.undefinedlabs.com/scopeagent v0.1.7
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,8 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/ziutek/rrd v0.0.3 h1:tGu7Dy0Z2Ij0qF7/7+fqWBZlM0j2Kp/RoTEG3+zHXjQ=
github.com/ziutek/rrd v0.0.3/go.mod h1:PAFbtWhFYrVeILz+2a6OKKdLYk8RlPJotQXlj7O0Z0A=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
Expand Down
14 changes: 14 additions & 0 deletions pkg/analyze/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,20 @@ func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileCont
}
return []*AnalyzeResult{result}, nil
}
if analyzer.RRD != nil {
isExcluded, err := isExcluded(analyzer.RRD.Exclude)
if err != nil {
return nil, err
}
if isExcluded {
return nil, nil
}
result, err := analyzeRRD(analyzer.RRD, findFiles)
if err != nil {
return nil, err
}
return []*AnalyzeResult{result}, nil
}
return nil, errors.New("invalid analyzer")

}
316 changes: 316 additions & 0 deletions pkg/analyze/rrd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
package analyzer

import (
"archive/tar"
"bytes"
"io"
"io/ioutil"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/ziutek/rrd"
)

type RRDSummary struct {
Load float64
}

func analyzeRRD(analyzer *troubleshootv1beta2.RRDAnalyze, getCollectedFileContents func(string) (map[string][]byte, error)) (*AnalyzeResult, error) {
rrdArchives, err := getCollectedFileContents("/collectd/rrd/*.tar")
if err != nil {
return nil, errors.Wrap(err, "failed to find rrd archives")
}

tmpDir, err := ioutil.TempDir("", "rrd")
if err != nil {
return nil, errors.Wrap(err, "failed to create temp rrd dir")
}
defer os.RemoveAll(tmpDir)

for name, data := range rrdArchives {
destDir := filepath.Join(tmpDir, filepath.Base(name))
if err := extractRRDFiles(data, destDir); err != nil {
return nil, errors.Wrap(err, "failed to extract rrd file")
}
}

loadFiles, err := findRRDLoadFiles(tmpDir)
if err != nil {
return nil, errors.Wrap(err, "failed to find load files")
}

rrdSummary := RRDSummary{
Load: 0,
}

// load files are always present, so this loop can be used for all host metrics
for _, loadFile := range loadFiles {
pathParts := strings.Split(loadFile, string(filepath.Separator))
if len(pathParts) < 3 {
continue
}

// .../<hostname>/load/load.rrd
hostname := pathParts[len(pathParts)-3]
hostDir := strings.TrimSuffix(loadFile, "/load/load.rrd")

hostLoad, err := getHostLoad(analyzer, loadFile, hostDir)
if err != nil {
return nil, errors.Wrapf(err, "failed to find analyze %s files", hostname)
}

rrdSummary.Load = math.Max(rrdSummary.Load, hostLoad)
}

result, err := getRRDAnalyzerOutcome(analyzer, rrdSummary)
if err != nil {
return nil, errors.Wrap(err, "failed to generate outcome")
}

return result, nil
}

func extractRRDFiles(archiveData []byte, dst string) error {
tarReader := tar.NewReader(bytes.NewReader(archiveData))
for {
header, err := tarReader.Next()
if err == io.EOF {
return nil
} else if err != nil {
return errors.Wrap(err, "failed to read rrd archive")
}

if header.Typeflag != tar.TypeReg {
continue
}

dstFileName := filepath.Join(dst, header.Name)

if err := os.MkdirAll(filepath.Dir(dstFileName), 0755); err != nil {
return errors.Wrap(err, "failed to create dest path")
}

err = func() error {
f, err := os.Create(dstFileName)
if err != nil {
return errors.Wrap(err, "failed to create dest file")
}
defer f.Close()

_, err = io.Copy(f, tarReader)
if err != nil {
return errors.Wrap(err, "failed to copy")
}
return nil
}()

if err != nil {
return errors.Wrap(err, "failed to write dest file")
}
}
}

func findRRDLoadFiles(rootDir string) ([]string, error) {
files := make([]string, 0)
err := filepath.Walk(rootDir, func(filename string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if filepath.Base(filename) == "load.rrd" {
files = append(files, filename)
}

return nil
})
if err != nil {
return nil, errors.Wrap(err, "failed to find rrd load files")
}

return files, nil
}

func getHostLoad(analyzer *troubleshootv1beta2.RRDAnalyze, loadFile string, hostRoot string) (float64, error) {
numberOfCPUs := 0
err := filepath.Walk(hostRoot, func(filename string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
return nil
}

if strings.HasPrefix(filepath.Base(filename), "cpu-") {
numberOfCPUs++
}
return nil
})
if err != nil {
return 0, errors.Wrap(err, "failed to find rrd files")
}

if numberOfCPUs == 0 {
numberOfCPUs = 1 // what else can we do here? return an error?
}

fileInfo, err := rrd.Info(loadFile)
if err != nil {
return 0, errors.Wrap(err, "failed to get rrd info")
}

// Query RRD data. Start and end have to be multiples of step.

window := 7 * 24 * time.Hour
step := 1 * time.Hour
lastUpdate := int64(fileInfo["last_update"].(uint))
endSeconds := int64(lastUpdate/int64(step.Seconds())) * int64(step.Seconds())
end := time.Unix(int64(endSeconds), 0)
start := end.Add(-window)
fetchResult, err := rrd.Fetch(loadFile, "MAX", start, end, step)
if err != nil {
return 0, errors.Wrap(err, "failed to fetch load data")
}
defer fetchResult.FreeValues()

values := fetchResult.Values()
maxLoad := float64(0)
for i := 0; i < len(values); i += 3 { // +3 because "shortterm", "midterm", "longterm"
v := values[i+1] // midterm
if math.IsNaN(v) {
continue
}
maxLoad = math.Max(maxLoad, values[i+1])
}

return maxLoad / float64(numberOfCPUs), nil
}

func getRRDAnalyzerOutcome(analyzer *troubleshootv1beta2.RRDAnalyze, rrdSummary RRDSummary) (*AnalyzeResult, error) {
collectorName := analyzer.CollectorName
if collectorName == "" {
collectorName = "rrd"
}

title := analyzer.CheckName
if title == "" {
title = collectorName
}
result := &AnalyzeResult{
Title: title,
IconKey: "host_load_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/rrd-analyze.svg",
}

for _, outcome := range analyzer.Outcomes {
if outcome.Fail != nil {
if outcome.Fail.When == "" {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI

return result, nil
}

isMatch, err := compareRRDConditionalToActual(outcome.Fail.When, rrdSummary)
if err != nil {
return result, errors.Wrap(err, "failed to compare rrd fail conditional")
}

if isMatch {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI

return result, nil
}
} else if outcome.Warn != nil {
if outcome.Pass.When == "" {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI

return result, nil
}

isMatch, err := compareRRDConditionalToActual(outcome.Warn.When, rrdSummary)
if err != nil {
return result, errors.Wrap(err, "failed to compare rrd warn conditional")
}

if isMatch {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI

return result, nil
}
} else if outcome.Pass != nil {
if outcome.Pass.When == "" {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI

return result, nil
}

isMatch, err := compareRRDConditionalToActual(outcome.Pass.When, rrdSummary)
if err != nil {
return result, errors.Wrap(err, "failed to compare rrd pass conditional")
}

if isMatch {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI

return result, nil
}
}
}

return result, nil
}

func compareRRDConditionalToActual(conditional string, rrdSummary RRDSummary) (bool, error) {
parts := strings.Split(strings.TrimSpace(conditional), " ")

if len(parts) != 3 {
return false, errors.New("unable to parse conditional")
}

switch parts[0] {
case "load":
expected, err := strconv.ParseFloat(parts[2], 64)
if err != nil {
return false, errors.Wrap(err, "failed to parse float")
}

switch parts[1] {
case "=", "==", "===":
return rrdSummary.Load == expected, nil
case "!=", "!==":
return rrdSummary.Load != expected, nil
case "<":
return rrdSummary.Load < expected, nil

case ">":
return rrdSummary.Load > expected, nil

case "<=":
return rrdSummary.Load <= expected, nil

case ">=":
return rrdSummary.Load >= expected, nil
}

return false, errors.Errorf("unknown rrd comparator: %q", parts[0])
}

return false, nil
}
8 changes: 8 additions & 0 deletions pkg/apis/troubleshoot/v1beta2/analyzer_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ type DatabaseAnalyze struct {
FileName string `json:"fileName,omitempty" yaml:"fileName,omitempty"`
}

type RRDAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
CollectorName string `json:"collectorName" yaml:"collectorName"`
FileName string `json:"fileName,omitempty" yaml:"fileName,omitempty"`
}

type AnalyzeMeta struct {
CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"`
Exclude multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"`
Expand All @@ -136,4 +143,5 @@ type Analyze struct {
Postgres *DatabaseAnalyze `json:"postgres,omitempty" yaml:"postgres,omitempty"`
Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"`
Redis *DatabaseAnalyze `json:"redis,omitempty" yaml:"redis,omitempty"`
RRD *RRDAnalyze `json:"rrd,omitempty" yaml:"rrd,omitempty"`
}

0 comments on commit 8ee4a09

Please sign in to comment.