From 68f132001fa97a22614c46810a8fff17b04f1641 Mon Sep 17 00:00:00 2001 From: Nick White Date: Thu, 17 Nov 2016 17:26:28 +0000 Subject: [PATCH] Add hsperfdata Input Plugin For gather data from the shared memory exposed by running processes. --- Godeps | 1 + README.md | 1 + plugins/inputs/all/all.go | 1 + plugins/inputs/hsperfdata/README.md | 39 ++++ plugins/inputs/hsperfdata/hsperfdata.go | 89 +++++++++ plugins/inputs/hsperfdata/hsperfdata_test.go | 180 ++++++++++++++++++ .../testdata/hsperfdata_tokuhirom/13223 | Bin 0 -> 32768 bytes .../testdata/hsperfdata_tokuhirom/21916 | Bin 0 -> 32768 bytes 8 files changed, 311 insertions(+) create mode 100644 plugins/inputs/hsperfdata/README.md create mode 100644 plugins/inputs/hsperfdata/hsperfdata.go create mode 100644 plugins/inputs/hsperfdata/hsperfdata_test.go create mode 100644 plugins/inputs/hsperfdata/testdata/hsperfdata_tokuhirom/13223 create mode 100644 plugins/inputs/hsperfdata/testdata/hsperfdata_tokuhirom/21916 diff --git a/Godeps b/Godeps index 6a0a17df1..ecb035772 100644 --- a/Godeps +++ b/Godeps @@ -51,6 +51,7 @@ github.com/shirou/gopsutil 1516eb9ddc5e61ba58874047a98f8b44b5e585e8 github.com/soniah/gosnmp 3fe3beb30fa9700988893c56a63b1df8e1b68c26 github.com/streadway/amqp b4f3ceab0337f013208d31348b578d83c0064744 github.com/stretchr/testify 1f4a1643a57e798696635ea4c126e9127adb7d3c +github.com/tokuhirom/go-hsperfdata 63efb7d3b4adbb23528abb6edcb7361a0c7ac22b github.com/vjeantet/grok 83bfdfdfd1a8146795b28e547a8e3c8b28a466c2 github.com/wvanbergen/kafka 46f9a1cf3f670edec492029fadded9c2d9e18866 github.com/wvanbergen/kazoo-go 0f768712ae6f76454f987c3356177e138df258f8 diff --git a/README.md b/README.md index 92ebf19ee..9ae131d72 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ configuration options. * [filestat](./plugins/inputs/filestat) * [haproxy](./plugins/inputs/haproxy) * [hddtemp](./plugins/inputs/hddtemp) +* [hsperfdata](./plugins/inputs/hsperfdata) (Hostpot JVMs) * [http_response](./plugins/inputs/http_response) * [httpjson](./plugins/inputs/httpjson) (generic JSON-emitting http service plugin) * [influxdb](./plugins/inputs/influxdb) diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 67b85905e..c359d03dd 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -32,6 +32,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/jolokia" _ "github.com/influxdata/telegraf/plugins/inputs/kafka_consumer" _ "github.com/influxdata/telegraf/plugins/inputs/kubernetes" + _ "github.com/influxdata/telegraf/plugins/inputs/hsperfdata" _ "github.com/influxdata/telegraf/plugins/inputs/leofs" _ "github.com/influxdata/telegraf/plugins/inputs/logparser" _ "github.com/influxdata/telegraf/plugins/inputs/lustre2" diff --git a/plugins/inputs/hsperfdata/README.md b/plugins/inputs/hsperfdata/README.md new file mode 100644 index 000000000..e9c9c52c2 --- /dev/null +++ b/plugins/inputs/hsperfdata/README.md @@ -0,0 +1,39 @@ +# hsperfdata Plugin + +The plugin gathers data from Hotspot JVMs via the hsperfdata files they expose. This plugin won't work if you've disabled their creation using `-XX:-UsePerfData` or `-XX:+PerfDisableSharedMem`! + +### Configuration: + +```toml +[[inputs.hsperfdata]] + # Optional: gather data from processes belonging to a different user. By + # default, the username in the USER environment variable is used to generate + # the hsperfdata directory name (usually "/tmp/hsperfdata_username") + user: "root" + + # use the named keys in the hsperfdata file as tags, not fields. By default, + # every key is exposed as a field. This example shows how to tag by JVM major + # version: + tags: ["java.property.java.vm.specification.version"] +``` + +### Measurements & Fields: + +All metrics are gathered as the "java" measurement. + +All keys in the hsperfdata file are exposed as fields; there's no comprehensive list as they vary by Hotspot version. + +### Tags: + +- All measurements have the following tags: + - pid (the process id of the monitored process) + - procname (the class name containing the `main` function being run) + +### Example Output: + +Most fields abbreviated; there's usually 200-300 of them: + +``` +$ ./telegraf -config telegraf.conf -input-filter example -test +java,host=nwhite91-mac,pid=17427,procname=com.sun.javaws.Main java.ci.totalTime="49874911809",...,sun.zip.zipFiles="28" 1479466710000000000 +``` diff --git a/plugins/inputs/hsperfdata/hsperfdata.go b/plugins/inputs/hsperfdata/hsperfdata.go new file mode 100644 index 000000000..a438f0f22 --- /dev/null +++ b/plugins/inputs/hsperfdata/hsperfdata.go @@ -0,0 +1,89 @@ +package hsperfdata + +import ( + "fmt" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" + "github.com/tokuhirom/go-hsperfdata/hsperfdata" +) + +type Hsperfdata struct { + User string + Tags []string +} + +var sampleConfig = ` + ## Use the hsperfdata directory belonging to a different user. + # user = "root" + # + ## Use the value for these keys in the hsperfdata as tags, not fields. By + ## default everything is a field. + # tags = ["sun.rt.jvmVersion"] +` + +func (n *Hsperfdata) SampleConfig() string { + return sampleConfig +} + +func (n *Hsperfdata) Repo() (*hsperfdata.Repository, error) { + if n.User == "" { + return hsperfdata.New() + } else { + return hsperfdata.NewUser(n.User) + } +} + +func (n *Hsperfdata) Gather(acc telegraf.Accumulator) error { + repo, err := n.Repo() + if err != nil { + return err + } + + files, err := repo.GetFiles() + if err != nil { + // the directory doesn't exist - so there aren't any Java processes running + return nil + } + + for _, file := range files { + result, err := file.Read() + if err != nil { + return err + } + + tags := map[string]string{"pid": file.GetPid()} + fields := result.GetMap() + + procname := result.GetProcName() + if procname != "" { + tags["procname"] = procname + } + + for _, tag := range n.Tags { + // don't tag metrics with "nil", just skip the tag if it's not there + if value, ok := fields[tag]; ok { + if valuestr, ok := value.(string); ok { + tags[tag] = valuestr + } else { + tags[tag] = fmt.Sprintf("%#v", fields[tag]) + } + delete(fields, tag) + } + } + + acc.AddFields("java", fields, tags) + } + + return nil +} + +func (n *Hsperfdata) Description() string { + return "Read performance data from running hotspot JVMs from shared memory" +} + +func init() { + inputs.Add("hsperfdata", func() telegraf.Input { + return &Hsperfdata{} + }) +} diff --git a/plugins/inputs/hsperfdata/hsperfdata_test.go b/plugins/inputs/hsperfdata/hsperfdata_test.go new file mode 100644 index 000000000..ff7541740 --- /dev/null +++ b/plugins/inputs/hsperfdata/hsperfdata_test.go @@ -0,0 +1,180 @@ +package hsperfdata + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + + "github.com/influxdata/telegraf/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const datadir = "hsperfdata_tokuhirom" + +func TestGatherNoTags(t *testing.T) { + setup() + defer teardown() + + hs := &Hsperfdata{User: "tokuhirom"} + + acc := testutil.Accumulator{} + require.NoError(t, hs.Gather(&acc)) + + assert.New(t).Equal(acc.NMetrics(), uint64(2)) + + acc.Lock() + defer acc.Unlock() + for _, p := range acc.Metrics { + if reflect.DeepEqual( + map[string]string{ + "pid": "13223", + }, + p.Tags) { + + assert.Equal( + t, + 212, + len(p.Fields)) + + // verify some of the fields (there's quite a lot!) + assert.Equal( + t, + "367001600", + p.Fields["sun.gc.generation.2.space.0.capacity"]) + assert.Equal( + t, + "/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Libraries", + p.Fields["sun.property.sun.boot.library.path"]) + + } else if reflect.DeepEqual(map[string]string{ + "pid": "21916", + "procname": "org.jetbrains.jps.cmdline.Launcher", + }, p.Tags) { + + assert.Equal( + t, + 253, + len(p.Fields), + fmt.Sprintf("wrong number of fields in %v", p)) + + assert.Equal( + t, + "3313990237", + p.Fields["java.ci.totalTime"]) + assert.Equal( + t, + "/Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/lib", + p.Fields["sun.property.sun.boot.library.path"]) + + } else { + msg := fmt.Sprintf("unknown with tags %v", p.Tags) + assert.Fail(t, msg) + } + } +} + +func TestGatherWithTags(t *testing.T) { + setup() + defer teardown() + + hs := &Hsperfdata{ + User: "tokuhirom", + Tags: []string{"java.property.java.vm.specification.vendor", "sun.gc.policy.minorCollectionSlope"}} + + acc := testutil.Accumulator{} + require.NoError(t, hs.Gather(&acc)) + + assert.New(t).Equal(acc.NMetrics(), uint64(2)) + + acc.Lock() + defer acc.Unlock() + for _, p := range acc.Metrics { + if reflect.DeepEqual( + map[string]string{ + "pid": "13223", + "java.property.java.vm.specification.vendor": "Sun Microsystems Inc.", + }, + p.Tags) { + + assert.Equal( + t, + 211, + len(p.Fields)) + + assert.NotContains( + t, + p.Fields, + "java.property.java.vm.specification.vendor", + "value promoted to tag") + + // verify some of the fields (there's quite a lot!) + assert.Equal( + t, + "367001600", + p.Fields["sun.gc.generation.2.space.0.capacity"]) + assert.Equal( + t, + "/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Libraries", + p.Fields["sun.property.sun.boot.library.path"]) + + } else if reflect.DeepEqual(map[string]string{ + "pid": "21916", + "procname": "org.jetbrains.jps.cmdline.Launcher", + "java.property.java.vm.specification.vendor": "Oracle Corporation", + "sun.gc.policy.minorCollectionSlope": "0", + }, p.Tags) { + + assert.Equal( + t, + 251, + len(p.Fields)) + + assert.NotContains( + t, + p.Fields, + "java.property.java.vm.specification.vendor", + "value promoted to tag") + + assert.Equal( + t, + "3313990237", + p.Fields["java.ci.totalTime"]) + assert.Equal( + t, + "/Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/lib", + p.Fields["sun.property.sun.boot.library.path"]) + + } else { + msg := fmt.Sprintf("unknown with tags %v", p.Tags) + assert.Fail(t, msg) + } + } +} + +func TestNoDirectoryNoMeasurements(t *testing.T) { + hs := &Hsperfdata{User: "tokuhirom"} + + acc := testutil.Accumulator{} + require.NoError(t, hs.Gather(&acc)) + assert.New(t).Equal(acc.NMetrics(), uint64(0)) +} + +func setup() { + _, filename, _, _ := runtime.Caller(1) + src := filepath.Join( + strings.Replace(filename, "hsperfdata_test.go", "testdata", 1), + datadir) + dest := filepath.Join( + os.TempDir(), + datadir) + os.Symlink(src, dest) +} + +func teardown() { + os.Remove(filepath.Join(os.TempDir(), datadir)) +} diff --git a/plugins/inputs/hsperfdata/testdata/hsperfdata_tokuhirom/13223 b/plugins/inputs/hsperfdata/testdata/hsperfdata_tokuhirom/13223 new file mode 100644 index 0000000000000000000000000000000000000000..53e73d2df9b504959b570d6326ff22cb4991c4d3 GIT binary patch literal 32768 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 SF;Fp3F;Fp3G4TJCf&T_gfExP% literal 0 HcmV?d00001