Newrelic Plugin

Adds a plugin to send new relic metric data. This is generally functional,
although not all kinks are worked out.

We currently use a "demo" uid for testing purposes.
This commit is contained in:
Daniel Hahn 2016-04-15 19:09:00 +02:00
parent c046232425
commit a627c348a4
11 changed files with 444 additions and 0 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ tivan
.idea
*~
*#
.tags

View File

@ -246,6 +246,7 @@ want to add support for another service or third-party API.
* [opentsdb](https://github.com/influxdata/telegraf/tree/master/plugins/outputs/opentsdb)
* [prometheus](https://github.com/influxdata/telegraf/tree/master/plugins/outputs/prometheus_client)
* [riemann](https://github.com/influxdata/telegraf/tree/master/plugins/outputs/riemann)
* [newrelic](https://github.com/influxdata/telegraf/tree/master/plugins/outputs/riemann)
## Contributing

View File

@ -14,6 +14,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/outputs/kinesis"
_ "github.com/influxdata/telegraf/plugins/outputs/librato"
_ "github.com/influxdata/telegraf/plugins/outputs/mqtt"
_ "github.com/influxdata/telegraf/plugins/outputs/newrelic"
_ "github.com/influxdata/telegraf/plugins/outputs/nsq"
_ "github.com/influxdata/telegraf/plugins/outputs/opentsdb"
_ "github.com/influxdata/telegraf/plugins/outputs/prometheus_client"

View File

@ -0,0 +1,128 @@
package newrelic
import (
"bytes"
"fmt"
"time"
"net/http"
// "io"
"encoding/json"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/outputs"
"github.com/influxdata/telegraf/internal"
)
type NewRelic struct {
ApiKey string
GuidBase string
Timeout internal.Duration
LastWrite time.Time
client *http.Client
}
var sampleConfig = `
## Your NewRelic Api Key
api_key = "XXXX" # required
## Guid base
## This allows you have unique GUIDs for your installation. This will generate
## a separate "plugin" GUID for each of the inputs that you use.
##
## see https://docs.newrelic.com/docs/plugins/plugin-developer-resources/planning-your-plugin/parts-plugin#guid
##
## This setting will allow you to "fork" your "plugins", and have your own
## dashboards and settings for them.
## The default behaviour is that the original author of the plugin sets up
## all the dashboards; other users cannot modify them.
## As it is very hard to provide useful defaults for all possible setup, we
## instead allow you to make your "own plugin" and modify the dashboards.
##
## The drawback is that your GUID must be unique, and that you must setup
## your own dashboards for everything.
##
## TODO: The default for this should be
## a "proper" GUID that is maintained to have reasonable default
# guid_base = 'my.domain.something.something' # TODO must still be implemented
## Metric Type TODO - Not yet implemented
##
## Can either be "Component" or "Custom"
##
## Component metrics are the default for plugins. They make the metrics
## available even to free accounts, but with the restrictions mentioned above.
##
## Custom metrics don't show up as plugins. They are freely usable in custom
## dashboards, but you need to have a paid subscription to see the data.
##
## Default is "Component"
# metric_type = "Custom"
`
func (nr *NewRelic) Connect() error {
if nr.ApiKey == "" {
return fmt.Errorf("apikey is a required field for newrelic output")
}
if nr.GuidBase == "" {
nr.GuidBase = "com.influxdata.demo-newrelic-agent"
}
nr.client = &http.Client{
Timeout: nr.Timeout.Duration,
}
nr.LastWrite = time.Now()
return nil
}
func (nr *NewRelic) Close() error {
return nil
}
func (nr *NewRelic) SampleConfig() string {
return sampleConfig
}
func (nr *NewRelic) Description() string {
return "Send telegraf metrics to NewRelic"
}
func (nr *NewRelic) PostPluginData(jsonData []byte) error {
req, reqErr := http.NewRequest("POST", "https://platform-api.newrelic.com/platform/v1/metrics", bytes.NewBuffer(jsonData))
if reqErr != nil { return reqErr }
req.Header.Add("Content-Type", "application/json")
req.Header.Add("X-License-Key", nr.ApiKey)
resp, respErr := nr.client.Do(req)
if respErr != nil { return respErr }
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 209 {
return fmt.Errorf("received bad status code, %d\n", resp.StatusCode)
}
return nil
}
func (nr *NewRelic) SendDataPage(dataPage interface{}) error {
cmpJson, err := json.Marshal(dataPage)
if err != nil { return err }
fmt.Println("Sending " + string(cmpJson) + " <")
return nr.PostPluginData(cmpJson)
}
func (nr *NewRelic) Write(metrics []telegraf.Metric) error {
data := NewRelicData{LastWrite: nr.LastWrite, Hosts: make(map[string][]NewRelicComponent)}
data.AddMetrics(metrics)
for _, dataPage := range(data.DataSets()) {
nr.SendDataPage(dataPage)
}
nr.LastWrite = time.Now()
return nil
}
func init() {
outputs.Add("newrelic", func() telegraf.Output { return &NewRelic{} })
}

View File

@ -0,0 +1,91 @@
package newrelic
import(
"fmt"
"encoding/json"
"os"
"strings"
"bytes"
"github.com/influxdata/telegraf"
)
type NewRelicComponent struct {
Duration int
TMetric telegraf.Metric
GuidBase string
tags *NewRelicTags
}
func (nrc NewRelicComponent) Tags() *NewRelicTags {
if nrc.tags == nil {
nrc.tags = &NewRelicTags{};
nrc.tags.Fill(nrc.TMetric.Tags())
}
return nrc.tags
}
func (nrc NewRelicComponent) Name() string {
return nrc.TMetric.Name()
}
func metricValue(value interface{}) int {
result := 0
switch value.(type) {
case int32:
result = int(value.(int32))
case int64:
result = int(value.(int64))
case float32:
result = int(value.(float32))
case float64:
result = int(value.(float64))
default:
result = 0
}
return result
}
func (nrc* NewRelicComponent) MetricName(originalName string) string {
var nameBuffer bytes.Buffer
nameBuffer.WriteString("Component/")
nameBuffer.WriteString(strings.Title(nrc.TMetric.Name()))
nameBuffer.WriteString("/")
nameBuffer.WriteString(strings.Title(originalName))
tags := nrc.Tags()
for _, key := range tags.SortedKeys {
nameBuffer.WriteString(fmt.Sprintf("/%s-%s", key, tags.GetTag(key)))
}
nameBuffer.WriteString("[Units]")
return nameBuffer.String()
}
func (nrc *NewRelicComponent) Metrics() map[string]int {
result := make(map[string]int)
for k,v := range(nrc.TMetric.Fields()) {
result[nrc.MetricName(k)] = metricValue(v)
}
return result
}
func (nrc NewRelicComponent) Hostname() string {
result := nrc.Tags().Hostname
if result == "" {
osname, err := os.Hostname()
if err == nil { result = "unknown" } else { result = osname }
}
return result
}
func (nrc *NewRelicComponent) Guid() string {
return fmt.Sprintf("%s-%s", nrc.GuidBase, strings.ToLower(nrc.TMetric.Name()))
}
func (nrc NewRelicComponent) MarshalJSON() ([]byte, error) {
myData := map[string]interface{} {
"name": nrc.Hostname(),
"guid": nrc.Guid(),
"duration": nrc.Duration,
"metrics": nrc.Metrics(),
}
return json.Marshal(myData)
}

View File

@ -0,0 +1,42 @@
package newrelic
import (
"testing"
// "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/outputs/newrelic"
// "github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/require"
)
func TestName(t *testing.T) {
dm := newrelic.DemoMetric{MyName: "Foo", TagList: newrelic.DemoTagList()}
component := newrelic.NewRelicComponent{TMetric: dm}
require.EqualValues(t, component.Name(), "Foo")
}
func TestGuid(t *testing.T) {
dm := newrelic.DemoMetric{MyName: "Lulu", TagList: newrelic.DemoTagList()}
component := newrelic.NewRelicComponent{TMetric: dm, GuidBase: "org.betterplace.telegraf-agent"}
require.EqualValues(t, component.Guid(), "org.betterplace.telegraf-agent-lulu")
}
func TestTags(t *testing.T) {
dm := newrelic.DemoMetric{MyName: "Lulu", TagList: newrelic.DemoTagList()}
component := newrelic.NewRelicComponent{TMetric: dm}
component.Tags()
}
func TestHostname(t *testing.T) {
tagList := newrelic.DemoTagList()
tagList["host"] = "baba"
dm := newrelic.DemoMetric{MyName: "Lulu", TagList: tagList}
component := newrelic.NewRelicComponent{TMetric: dm}
require.EqualValues(t, component.Hostname(), "baba")
}
func TestMetricName(t *testing.T) {
tagList := newrelic.DemoTagList()
dm := newrelic.DemoMetric{MyName: "Lulu", TagList: tagList}
component := newrelic.NewRelicComponent{TMetric: dm}
require.EqualValues(t, component.MetricName("fnord"), "Component/Lulu/Fnord/Fluff-naa/Hoof-bar/Zoo-goo[Units]")
}

View File

@ -0,0 +1,35 @@
package newrelic
import(
"time"
"github.com/influxdata/telegraf"
)
type NewRelicData struct {
LastWrite time.Time
Hosts map[string][]NewRelicComponent
GuidBase string
}
func (nrd *NewRelicData) AddMetric(metric telegraf.Metric) {
component := NewRelicComponent{
Duration: int(time.Since(nrd.LastWrite).Seconds()),
TMetric: metric,
GuidBase: nrd.GuidBase}
host := component.Hostname()
nrd.Hosts[host] = append(nrd.Hosts[host],component)
}
func (nrd *NewRelicData) AddMetrics(metrics []telegraf.Metric) {
for _, metric := range(metrics) {
nrd.AddMetric(metric)
}
}
func (nrd *NewRelicData) DataSets() []interface{} {
result := make([]interface{}, 0)
for host, components := range(nrd.Hosts) {
result = append(result, map[string]interface{} { "agent": map[string]string { "host": host, "version": "0.0.1" }, "components": components })
}
return result
}

View File

@ -0,0 +1,35 @@
package newrelic
import (
"testing"
"time"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/outputs/newrelic"
// "github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/require"
)
func TestAddMetrics(t *testing.T) {
dm := []telegraf.Metric{newrelic.DemoMetric{MyName: "Foo", TagList: newrelic.DemoTagList()}}
data := newrelic.NewRelicData{
LastWrite: time.Now(),
Hosts: make(map[string][]newrelic.NewRelicComponent),
GuidBase: "org.betterplace.test-foo"}
data.AddMetrics(dm)
require.EqualValues(t, len(data.Hosts), 1)
}
func TestDataSets(t *testing.T) {
dm := []telegraf.Metric{newrelic.DemoMetric{MyName: "Foo", TagList: newrelic.DemoTagList()}}
data := newrelic.NewRelicData{
LastWrite: time.Now(),
Hosts: make(map[string][]newrelic.NewRelicComponent),
GuidBase: "org.betterplace.test-foo"}
data.AddMetrics(dm)
sets := data.DataSets()
require.EqualValues(t, len(sets), 1)
set, _ := sets[0].(map[string]interface{})
agent := set["agent"].(map[string]string)
require.EqualValues(t, agent["host"], "Hulu")
}

View File

@ -0,0 +1,45 @@
package newrelic
import(
"sort"
"strings"
)
type NewRelicTags struct {
Tags *map[string]string
SortedKeys []string
Hostname string
}
func TagValue(tagValue string) string {
tagValueParts := strings.Split(tagValue, "/")
var clean_parts []string
for _, part := range(tagValueParts) {
if part != "" { clean_parts = append(clean_parts, part) }
}
if len(clean_parts) > 0 {
tagValue = strings.ToLower(strings.Join(clean_parts, "-"))
} else {
tagValue = "root"
}
return tagValue
}
func (nrt *NewRelicTags) Fill(originalTags map[string]string) {
nrt.SortedKeys = make([]string, 0, len(originalTags))
tags := make(map[string]string)
nrt.Tags = &tags
for key, value := range originalTags {
if key != "host" {
nrt.SortedKeys = append(nrt.SortedKeys, key)
(*nrt.Tags)[key] = TagValue(value)
} else {
nrt.Hostname = value
}
}
sort.Strings(nrt.SortedKeys)
}
func (nrt *NewRelicTags) GetTag(tagKey string) string {
return (*nrt.Tags)[tagKey]
}

View File

@ -0,0 +1,36 @@
package newrelic
import (
"testing"
"github.com/influxdata/telegraf/plugins/outputs/newrelic"
// "github.com/influxdata/telegraf/testutil"
"github.com/stretchr/testify/require"
)
func TestTagValue(t *testing.T) {
require.EqualValues(t, newrelic.TagValue("Hello/World"), "hello-world")
require.EqualValues(t, newrelic.TagValue("/Hello/World"), "hello-world")
require.EqualValues(t, newrelic.TagValue(""), "root")
}
func TestFillSimple(t *testing.T) {
tags := &newrelic.NewRelicTags{}
tags.Fill(newrelic.DemoTagList())
require.EqualValues(t, tags.SortedKeys, []string{"Fluff", "Hoof", "Zoo"})
}
func TestFillWithHost(t *testing.T) {
tags := &newrelic.NewRelicTags{}
demoList := newrelic.DemoTagList()
demoList["host"] = "hulu"
tags.Fill(demoList)
require.EqualValues(t, tags.SortedKeys, []string{"Fluff", "Hoof", "Zoo"})
require.EqualValues(t, tags.Hostname, "hulu")
}
func TestFillAndGetTag(t *testing.T) {
tags := &newrelic.NewRelicTags{}
tags.Fill(newrelic.DemoTagList())
require.EqualValues(t, tags.GetTag("Zoo"), "goo")
}

View File

@ -0,0 +1,29 @@
package newrelic
import (
"time"
"github.com/influxdata/influxdb/client/v2"
)
func DemoTagList() map[string]string {
return map[string]string{
"Fluff": "Naa",
"Hoof": "Bar",
"Zoo": "Goo",
"host": "Hulu",
}
}
type DemoMetric struct {
MyName string
TagList map[string]string
}
func (dm DemoMetric) Name() string { return dm.MyName }
func (dm DemoMetric) Tags() map[string]string { return dm.TagList }
func (dm DemoMetric) Time() time.Time { return time.Now() }
func (dm DemoMetric) UnixNano() int64 { return 0 }
func (dm DemoMetric) Fields() map[string]interface{} { return nil }
func (dm DemoMetric) String() string { return "StringRepresenation" }
func (dm DemoMetric) PrecisionString(precison string) string { return "PrecisionString" }
func (dm DemoMetric) Point() *client.Point { return nil }