forked from influxdata/telegraf
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add newrelic output plugin (influxdata#7019)
- Loading branch information
1 parent
7b33ef0
commit 580ac61
Showing
7 changed files
with
366 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
#New Relic output plugin | ||
|
||
This plugins writes to New Relic insights. | ||
|
||
``` | ||
[[outputs.newrelic]] | ||
## New Relic Insights API key | ||
insights_key = "insights api key" | ||
# metric_prefix if defined, prefix's metrics name for easy identification | ||
# metric_prefix = "" | ||
# harvest timeout, default is 15 seconds | ||
# timeout = "15s" | ||
``` | ||
####Parameters | ||
|
||
|Parameter Name|Type|Description| | ||
|:-|:-|:-| | ||
| insights_key | Required | Insights API Insert key | | ||
| metric_prefix | Optional | If defined, prefix's metrics name for easy identification | | ||
| timeout | Optional | If defined, changes harvest timeout | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package newrelic | ||
|
||
// newrelic.go | ||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/influxdata/telegraf" | ||
"github.com/influxdata/telegraf/internal" | ||
"github.com/influxdata/telegraf/plugins/outputs" | ||
"github.com/newrelic/newrelic-telemetry-sdk-go/cumulative" | ||
"github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" | ||
) | ||
|
||
// NewRelic nr structure | ||
type NewRelic struct { | ||
harvestor *telemetry.Harvester | ||
dc *cumulative.DeltaCalculator | ||
InsightsKey string `toml:"insights_key"` | ||
MetricPrefix string `toml:"metric_prefix"` | ||
Timeout internal.Duration `toml:"timeout"` | ||
savedErrors map[int]interface{} | ||
errorCount int | ||
Client http.Client | ||
} | ||
|
||
// Description returns a one-sentence description on the Output | ||
func (nr *NewRelic) Description() string { | ||
return "Send metrics to New Relic metrics endpoint" | ||
} | ||
|
||
// SampleConfig : return default configuration of the Output | ||
func (nr *NewRelic) SampleConfig() string { | ||
return ` | ||
## New Relic Insights API key (required) | ||
insights_key = "insights api key" | ||
# metric_prefix if defined, prefix's metrics name for easy identification (optional) | ||
# metric_prefix = "" | ||
# harvest timeout, default is 15 seconds | ||
# timeout = "15s" | ||
` | ||
} | ||
|
||
// Connect to the Output | ||
func (nr *NewRelic) Connect() error { | ||
if nr.InsightsKey == "" { | ||
return fmt.Errorf("InsightKey is a required for newrelic") | ||
} | ||
var err error | ||
nr.harvestor, err = telemetry.NewHarvester(telemetry.ConfigAPIKey(nr.InsightsKey), | ||
telemetry.ConfigHarvestPeriod(0), | ||
func(cfg *telemetry.Config) { | ||
cfg.Product = "NewRelic-Telegraf-Plugin" | ||
cfg.ProductVersion = "1.0" | ||
cfg.HarvestTimeout = nr.Timeout.Duration | ||
cfg.Client = &nr.Client | ||
cfg.ErrorLogger = func(e map[string]interface{}) { | ||
var errorString string | ||
for k, v := range e { | ||
errorString += fmt.Sprintf("%s = %s ", k, v) | ||
} | ||
nr.errorCount++ | ||
nr.savedErrors[nr.errorCount] = errorString | ||
} | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("unable to connect to newrelic %v", err) | ||
} | ||
|
||
nr.dc = cumulative.NewDeltaCalculator() | ||
return nil | ||
} | ||
|
||
// Close any connections to the Output | ||
func (nr *NewRelic) Close() error { | ||
|
||
nr.errorCount = 0 | ||
nr.Client.CloseIdleConnections() | ||
return nil | ||
} | ||
|
||
// Write takes in group of points to be written to the Output | ||
func (nr *NewRelic) Write(metrics []telegraf.Metric) error { | ||
|
||
nr.errorCount = 0 | ||
nr.savedErrors = make(map[int]interface{}) | ||
|
||
for _, metric := range metrics { | ||
// create tag map | ||
tags := make(map[string]interface{}) | ||
for _, tag := range metric.TagList() { | ||
tags[tag.Key] = tag.Value | ||
} | ||
for _, field := range metric.FieldList() { | ||
var mvalue float64 | ||
var mname string | ||
if nr.MetricPrefix != "" { | ||
mname = nr.MetricPrefix + "." + metric.Name() + "." + field.Key | ||
} else { | ||
mname = metric.Name() + "." + field.Key | ||
} | ||
switch n := field.Value.(type) { | ||
case int64: | ||
mvalue = float64(n) | ||
case uint64: | ||
mvalue = float64(n) | ||
case float64: | ||
mvalue = float64(n) | ||
case bool: | ||
mvalue = float64(0) | ||
if n { | ||
mvalue = float64(1) | ||
} | ||
case string: | ||
// Do not log everytime we encounter string | ||
// we just skip | ||
continue | ||
default: | ||
return fmt.Errorf("Undefined field type: %T", field.Value) | ||
} | ||
|
||
switch metric.Type() { | ||
case telegraf.Counter: | ||
if counter, ok := nr.dc.CountMetric(mname, tags, mvalue, metric.Time()); ok { | ||
nr.harvestor.RecordMetric(counter) | ||
} | ||
default: | ||
nr.harvestor.RecordMetric(telemetry.Gauge{ | ||
Timestamp: metric.Time(), | ||
Value: mvalue, | ||
Name: mname, | ||
Attributes: tags}) | ||
} | ||
} | ||
} | ||
// By default, the Harvester sends metrics and spans to the New Relic | ||
// backend every 5 seconds. You can force data to be sent at any time | ||
// using HarvestNow. | ||
nr.harvestor.HarvestNow(context.Background()) | ||
|
||
//Check if we encountered errors | ||
if nr.errorCount != 0 { | ||
return fmt.Errorf("unable to harvest metrics %s ", nr.savedErrors[nr.errorCount]) | ||
} | ||
return nil | ||
} | ||
|
||
func init() { | ||
outputs.Add("newrelic", func() telegraf.Output { | ||
return &NewRelic{ | ||
Timeout: internal.Duration{Duration: time.Second * 15}, | ||
Client: http.Client{}, | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
package newrelic | ||
|
||
import ( | ||
"math" | ||
"testing" | ||
"time" | ||
|
||
"github.com/influxdata/telegraf" | ||
"github.com/influxdata/telegraf/internal" | ||
"github.com/influxdata/telegraf/testutil" | ||
"github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestBasic(t *testing.T) { | ||
nr := &NewRelic{ | ||
MetricPrefix: "Test", | ||
InsightsKey: "12345", | ||
Timeout: internal.Duration{Duration: time.Second * 5}, | ||
} | ||
if testing.Short() { | ||
t.Skip("skipping test in short mode.") | ||
} | ||
|
||
err := nr.Connect() | ||
require.NoError(t, err) | ||
|
||
err = nr.Write(testutil.MockMetrics()) | ||
assert.Contains(t, err.Error(), "unable to harvest metrics") | ||
} | ||
|
||
func TestNewRelic_Write(t *testing.T) { | ||
type args struct { | ||
metrics []telegraf.Metric | ||
} | ||
tests := []struct { | ||
name string | ||
metrics []telegraf.Metric | ||
auditMessage string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "Test: Basic mock metric write", | ||
metrics: testutil.MockMetrics(), | ||
wantErr: false, | ||
auditMessage: `"metrics":[{"name":"test1.value","type":"gauge","value":1,"timestamp":1257894000000,"attributes":{"tag1":"value1"}}]`, | ||
}, | ||
{ | ||
name: "Test: Test string ", | ||
metrics: []telegraf.Metric{ | ||
testutil.TestMetric("value1", "test_String"), | ||
}, | ||
wantErr: false, | ||
auditMessage: "", | ||
}, | ||
{ | ||
name: "Test: Test int64 ", | ||
metrics: []telegraf.Metric{ | ||
testutil.TestMetric(int64(15), "test_int64"), | ||
}, | ||
wantErr: false, | ||
auditMessage: `"metrics":[{"name":"test_int64.value","type":"gauge","value":15,"timestamp":1257894000000,"attributes":{"tag1":"value1"}}]`, | ||
}, | ||
{ | ||
name: "Test: Test uint64 ", | ||
metrics: []telegraf.Metric{ | ||
testutil.TestMetric(uint64(20), "test_uint64"), | ||
}, | ||
wantErr: false, | ||
auditMessage: `"metrics":[{"name":"test_uint64.value","type":"gauge","value":20,"timestamp":1257894000000,"attributes":{"tag1":"value1"}}]`, | ||
}, | ||
{ | ||
name: "Test: Test bool true ", | ||
metrics: []telegraf.Metric{ | ||
testutil.TestMetric(bool(true), "test_bool_true"), | ||
}, | ||
wantErr: false, | ||
auditMessage: `"metrics":[{"name":"test_bool_true.value","type":"gauge","value":1,"timestamp":1257894000000,"attributes":{"tag1":"value1"}}]`, | ||
}, | ||
{ | ||
name: "Test: Test bool false ", | ||
metrics: []telegraf.Metric{ | ||
testutil.TestMetric(bool(false), "test_bool_false"), | ||
}, | ||
wantErr: false, | ||
auditMessage: `"metrics":[{"name":"test_bool_false.value","type":"gauge","value":0,"timestamp":1257894000000,"attributes":{"tag1":"value1"}}]`, | ||
}, | ||
{ | ||
name: "Test: Test max float64 ", | ||
metrics: []telegraf.Metric{ | ||
testutil.TestMetric(math.MaxFloat64, "test_maxfloat64"), | ||
}, | ||
wantErr: false, | ||
auditMessage: `"metrics":[{"name":"test_maxfloat64.value","type":"gauge","value":1.7976931348623157e+308,"timestamp":1257894000000,"attributes":{"tag1":"value1"}}]`, | ||
}, | ||
{ | ||
name: "Test: Test NAN ", | ||
metrics: []telegraf.Metric{ | ||
testutil.TestMetric(math.NaN, "test_NaN"), | ||
}, | ||
wantErr: false, | ||
auditMessage: ``, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
var auditLog map[string]interface{} | ||
nr := &NewRelic{} | ||
nr.harvestor, _ = telemetry.NewHarvester( | ||
telemetry.ConfigHarvestPeriod(0), | ||
func(cfg *telemetry.Config) { | ||
cfg.APIKey = "dummyTestKey" | ||
cfg.HarvestPeriod = 0 | ||
cfg.HarvestTimeout = 0 | ||
cfg.AuditLogger = func(e map[string]interface{}) { | ||
auditLog = e | ||
} | ||
}) | ||
err := nr.Write(tt.metrics) | ||
assert.NoError(t, err) | ||
if auditLog["data"] != nil { | ||
assert.Contains(t, auditLog["data"], tt.auditMessage) | ||
} else { | ||
assert.Contains(t, "", tt.auditMessage) | ||
} | ||
|
||
if (err != nil) != tt.wantErr { | ||
t.Errorf("NewRelic.Write() error = %v, wantErr %v", err, tt.wantErr) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestNewRelic_Connect(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
newrelic *NewRelic | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "Test: No Insights key", | ||
newrelic: &NewRelic{ | ||
MetricPrefix: "prefix", | ||
}, | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "Test: Insights key", | ||
newrelic: &NewRelic{ | ||
InsightsKey: "12312133", | ||
MetricPrefix: "prefix", | ||
}, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "Test: Only Insights key", | ||
newrelic: &NewRelic{ | ||
InsightsKey: "12312133", | ||
}, | ||
wantErr: false, | ||
}, | ||
{ | ||
name: "Test: Insights key and Timeout", | ||
newrelic: &NewRelic{ | ||
InsightsKey: "12312133", | ||
Timeout: internal.Duration{Duration: time.Second * 5}, | ||
}, | ||
wantErr: false, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
nr := tt.newrelic | ||
if err := nr.Connect(); (err != nil) != tt.wantErr { | ||
t.Errorf("NewRelic.Connect() error = %v, wantErr %v", err, tt.wantErr) | ||
} | ||
}) | ||
} | ||
} |