From 66b589b25d87533e85aa6c55518bd68c252ebdde Mon Sep 17 00:00:00 2001 From: Illya Chekrygin Date: Thu, 12 May 2016 16:13:59 -0700 Subject: [PATCH] Add Appdynamics output plugin --- plugins/outputs/all/all.go | 1 + plugins/outputs/appdynamics/README.md | 17 +++ plugins/outputs/appdynamics/appdynamics.go | 127 ++++++++++++++++++ .../outputs/appdynamics/appdynamics_test.go | 100 ++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 plugins/outputs/appdynamics/README.md create mode 100644 plugins/outputs/appdynamics/appdynamics.go create mode 100644 plugins/outputs/appdynamics/appdynamics_test.go diff --git a/plugins/outputs/all/all.go b/plugins/outputs/all/all.go index 18fb1c925..004fe5344 100644 --- a/plugins/outputs/all/all.go +++ b/plugins/outputs/all/all.go @@ -3,6 +3,7 @@ package all import ( _ "github.com/influxdata/telegraf/plugins/outputs/amon" _ "github.com/influxdata/telegraf/plugins/outputs/amqp" + _ "github.com/influxdata/telegraf/plugins/outputs/appdynamics" _ "github.com/influxdata/telegraf/plugins/outputs/cloudwatch" _ "github.com/influxdata/telegraf/plugins/outputs/datadog" _ "github.com/influxdata/telegraf/plugins/outputs/file" diff --git a/plugins/outputs/appdynamics/README.md b/plugins/outputs/appdynamics/README.md new file mode 100644 index 000000000..e55eb0a07 --- /dev/null +++ b/plugins/outputs/appdynamics/README.md @@ -0,0 +1,17 @@ +# Appdynamics Output Plugin + +This plugin writes to [Appdynamics Machine Agent](http://localhost:8293) +via raw TCP. + +## Configuration: + +```toml + ## controller information tor connect and retrieve tier-id value + controllerTierURL = "https://foo.saas.appdynamics.com/controller/rest/applications/bar/tiers/baz?output=JSON" + controllerUserName = "apiuser" + controllerPassword = "apipass" + ## Machine agent custom metrics listener url format string + ## |Component:%d| gets transformed into |Component:id| during initialization - where 'id' is a tier-id for + ## this controller application/tier combination + agentURL = "http://localhost:8293/machineagent/metrics?name=Server|Component:%d|Custom+Metrics|" +``` \ No newline at end of file diff --git a/plugins/outputs/appdynamics/appdynamics.go b/plugins/outputs/appdynamics/appdynamics.go new file mode 100644 index 000000000..85d811785 --- /dev/null +++ b/plugins/outputs/appdynamics/appdynamics.go @@ -0,0 +1,127 @@ +package influxdb + +import ( + "encoding/json" + "fmt" + "github.com/influxdata/telegraf" + "io/ioutil" + "log" + "net/http" + "github.com/influxdata/telegraf/plugins/outputs" +) + +var sampleConfig = ` + ## controller information tor connect and retrieve tier-id value + controllerTierURL = "https://foo.saas.appdynamics.com/controller/rest/applications/bar/tiers/baz?output=JSON" + controllerUserName = "apiuser@account.com" + controllerPassword = "apipassword" + ## Machine agent custom metrics listener url format string + ## |Component:%d| gets transformed into |Component:id| during initialization - where 'id' is a tier-id for + ## this controller application/tier combination + agentURL = "http://localhost:8293/machineagent/metrics?name=Server|Component:%d|Custom+Metrics|" +` + +type Appdynamics struct { + // Controller values for retrieving tier-id from the controller + ControllerTierURL string + ControllerUserName string + ControllerPassword string + + // Machine agent URL format string + AgentURL string + + // Tier id value retrieved from the controller for this application/tier + tierId int64 +} + +// Close - There is nothing to close here, but need to comply with output interface +func (a *Appdynamics) Close() error{ + return nil +} + +// Connect - initialize appdynamics plugin by retrieving tier-id value from the appdynamics controller +// for this application/tier combination and updating (reformatting) agent url string with tier-id value +func (a *Appdynamics) Connect() (err error) { + a.tierId, err = a.getTierId() + if err != nil { + return err + } + fmt.Printf("Agent Tier ID: %d\n", a.tierId) + a.AgentURL = fmt.Sprintf(a.AgentURL, a.tierId) + fmt.Printf("Agent URL: %s\n", a.AgentURL) + return err +} + +// Description - describing what this is +func (a *Appdynamics) Description() string { + return "Configuration for Appdynamics controller/listener to send metrics to" +} + +// getTierId - retrieve tier id value for this application/tier combination from the appdynamics controller +func (a *Appdynamics) getTierId() (int64, error) { + client := &http.Client{} + + /* Auth */ + req, err := http.NewRequest("GET", a.ControllerTierURL, nil) + req.SetBasicAuth(a.ControllerUserName, a.ControllerPassword) + + res, err := client.Do(req) + if err != nil { + return 0, err + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return 0, err + } + + var tiers []struct { + Id int64 `json:"id"` + } + + err = json.Unmarshal(body, &tiers) + if err != nil { + return 0, err + } + if len(tiers) != 1 { + fmt.Println("Invalid reply: ", tiers) + } + + return tiers[0].Id, nil +} + +// Write - post telegraf metrics to appdynamics machine agent listener using +// http.Get per https://docs.appdynamics.com/display/PRO40/Standalone+Machine+Agent+HTTP+Listener +func (a *Appdynamics) Write(metrics []telegraf.Metric) error { + for _, metric := range metrics { + if metric.Fields()["value"] == nil { + log.Println("WARNING: missing value:", metric) + } else { + var appdType string + switch metric.Tags()["metric_type"] { + case "gauge": + appdType = "average" + default: + appdType = "sum" + } + url := a.AgentURL + metric.Name() + fmt.Sprintf("&value=%v&type=%s", metric.Fields()["value"], appdType) + fmt.Printf("Calling %s ...\n", url) + _, err := http.Get(url) + if err != nil { + log.Println("ERROR: " + err.Error()) + } + } + } + return nil +} + +func (a *Appdynamics) SampleConfig() string { + return sampleConfig +} + +func init() { + outputs.Add("appdynamics", func() telegraf.Output { + return &Appdynamics{} + }) +} diff --git a/plugins/outputs/appdynamics/appdynamics_test.go b/plugins/outputs/appdynamics/appdynamics_test.go new file mode 100644 index 000000000..90ef9384e --- /dev/null +++ b/plugins/outputs/appdynamics/appdynamics_test.go @@ -0,0 +1,100 @@ +package influxdb + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/influxdata/telegraf" + "github.com/stretchr/testify/assert" +) + +// TestAppdynamicsError - attemp to initialize Appdynamics with invalid controller user name value +func TestAppdynamicsError(t *testing.T) { + a := Appdynamics{ + ControllerTierURL: "https://foo.saas.appdynamics.com/controller/rest/applications/bar/tiers/baz?output=JSON", + ControllerUserName: "apiuser@foo.bar.com", + ControllerPassword: "pass123", + AgentURL: "http://localhost:8293/machineagent/metrics?name=Server|Component:%d|Custom+Metrics|", + } + assert.Error(t, a.Connect()) +} + +// TestAppdynamicsOK - successfully initialize Appdynamics and process metrics calls +func TestAppdynamicsOK(t *testing.T) { + // channel to collect received calls + ch := make(chan string, 1) + + h := func(w http.ResponseWriter, r *http.Request) { + s := r.URL.String() + fmt.Fprintf(w, "Hi there, I love %s!", s) + ch <- r.URL.RawQuery + } + http.HandleFunc("/", h) + go http.ListenAndServe(":8293", nil) + time.Sleep(time.Millisecond * 100) + + a := Appdynamics{ + ControllerTierURL: "https://foo.saas.appdynamics.com/controller/rest/applications/bar/tiers/baz?output=JSON", + ControllerUserName: "apiuser@foo.bar", + ControllerPassword: "pass123", + AgentURL: "http://localhost:8293/machineagent/metrics?name=Server|Component:%d|Custom+Metrics|", + } + // this error is expected since we are not connecting to actual controller + assert.Error(t, a.Connect()) + // reset agent url value with '123' tier id + a.AgentURL = fmt.Sprintf(a.AgentURL, 123) + assert.Equal(t, a.AgentURL, "http://localhost:8293/machineagent/metrics?name=Server|Component:123|Custom+Metrics|") + + // counter type - appd-type: sum + m, _ := telegraf.NewMetric( + "foo", + map[string]string{"metrcic_type": "counter"}, + map[string]interface{}{"value": float64(1.23)}, + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), + ) + metrics := []telegraf.Metric{m} + assert.NoError(t, a.Write(metrics)) + call := <-ch + assert.Equal(t, "name=Server|Component:123|Custom+Metrics|foo&value=1.23&type=sum", call) + + // gauge type - appd-type: average + m, _ = telegraf.NewMetric( + "foo", + map[string]string{"metric_type": "gauge"}, + map[string]interface{}{"value": float64(4.56)}, + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), + ) + metrics = []telegraf.Metric{m} + assert.NoError(t, a.Write(metrics)) + call = <-ch + assert.Equal(t, "name=Server|Component:123|Custom+Metrics|foo&value=4.56&type=average", call) + + // other type - defaults to appd-type: sum + m, _ = telegraf.NewMetric( + "foo", + map[string]string{"metric_type": "bar"}, + map[string]interface{}{"value": float64(7.89)}, + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), + ) + metrics = []telegraf.Metric{m} + assert.NoError(t, a.Write(metrics)) + call = <-ch + assert.Equal(t, "name=Server|Component:123|Custom+Metrics|foo&value=7.89&type=sum", call) + + // invalid: missing value + m, _ = telegraf.NewMetric( + "foo", + map[string]string{"metric_type": "bar"}, + map[string]interface{}{"values": float64(7.89)}, + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC), + ) + metrics = []telegraf.Metric{m} + assert.NoError(t, a.Write(metrics)) + select { + case call = <-ch: + t.Error("No messages expected, but got: ", call) + default: + } +}