From d9deb266df4ba10e2ee4d93c984c32ffccb0d736 Mon Sep 17 00:00:00 2001 From: Rodolphe Blancho Date: Wed, 9 Aug 2017 04:48:01 +1000 Subject: [PATCH] Add salesforce input plugin (#3075) --- plugins/inputs/all/all.go | 1 + plugins/inputs/salesforce/README.md | 48 ++ plugins/inputs/salesforce/salesforce.go | 245 +++++++++ plugins/inputs/salesforce/salesforce_test.go | 515 +++++++++++++++++++ 4 files changed, 809 insertions(+) create mode 100644 plugins/inputs/salesforce/README.md create mode 100644 plugins/inputs/salesforce/salesforce.go create mode 100644 plugins/inputs/salesforce/salesforce_test.go diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index dd3c178d9..8b99b1906 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -72,6 +72,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/redis" _ "github.com/influxdata/telegraf/plugins/inputs/rethinkdb" _ "github.com/influxdata/telegraf/plugins/inputs/riak" + _ "github.com/influxdata/telegraf/plugins/inputs/salesforce" _ "github.com/influxdata/telegraf/plugins/inputs/sensors" _ "github.com/influxdata/telegraf/plugins/inputs/snmp" _ "github.com/influxdata/telegraf/plugins/inputs/snmp_legacy" diff --git a/plugins/inputs/salesforce/README.md b/plugins/inputs/salesforce/README.md new file mode 100644 index 000000000..5ee0f6a3d --- /dev/null +++ b/plugins/inputs/salesforce/README.md @@ -0,0 +1,48 @@ +# Salesforce Input Plugin + +The Salesforce plugin gathers metrics about the limits in your Salesforce organization and the remaining usage. +It fetches its data from the [limits endpoint](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_limits.htm) of Salesforce's REST API. + +### Configuration: + +```toml +# Gather Metrics about Salesforce limits and remaining usage +[[inputs.salesforce]] + username = "your_username" + password = "your_password" + ## (Optional) security tokjen + security_token = "your_security_token" + ## (Optional) environment type (sandbox or production) + ## default is: production + # environment = "production" + ## (Optional) API version (default: "39.0") + # version = "39.0" +``` + +### Measurements & Fields: + +Salesforce provide one measurment named "salesforce". +Each entry is converted to snake\_case and 2 fields are created. + +- \_max represents the limit threshold +- \_remaining represents the usage remaining before hitting the limit threshold + +- salesforce + - \_max (int) + - \_remaining (int) + - (...) + +### Tags: + +- All measurements have the following tags: + - host + - organization_id (t18 char organisation ID) + + +### Example Output: + +``` +$./telegraf --config telegraf.conf --input-filter salesforce --test + +salesforce,organization_id=XXXXXXXXXXXXXXXXXX,host=xxxxx.salesforce.com daily_workflow_emails_max=546000i,hourly_time_based_workflow_max=50i,daily_async_apex_executions_remaining=250000i,daily_durable_streaming_api_events_remaining=1000000i,streaming_api_concurrent_clients_remaining=2000i,daily_bulk_api_requests_remaining=10000i,hourly_sync_report_runs_remaining=500i,daily_api_requests_max=5000000i,data_storage_mb_remaining=1073i,file_storage_mb_remaining=1069i,daily_generic_streaming_api_events_remaining=10000i,hourly_async_report_runs_remaining=1200i,hourly_time_based_workflow_remaining=50i,daily_streaming_api_events_remaining=1000000i,single_email_max=5000i,hourly_dashboard_refreshes_remaining=200i,streaming_api_concurrent_clients_max=2000i,daily_durable_generic_streaming_api_events_remaining=1000000i,daily_api_requests_remaining=4999998i,hourly_dashboard_results_max=5000i,hourly_async_report_runs_max=1200i,daily_durable_generic_streaming_api_events_max=1000000i,hourly_dashboard_results_remaining=5000i,concurrent_sync_report_runs_max=20i,durable_streaming_api_concurrent_clients_remaining=2000i,daily_workflow_emails_remaining=546000i,hourly_dashboard_refreshes_max=200i,daily_streaming_api_events_max=1000000i,hourly_sync_report_runs_max=500i,hourly_o_data_callout_max=10000i,mass_email_max=5000i,mass_email_remaining=5000i,single_email_remaining=5000i,hourly_dashboard_statuses_max=999999999i,concurrent_async_get_report_instances_max=200i,daily_durable_streaming_api_events_max=1000000i,daily_generic_streaming_api_events_max=10000i,hourly_o_data_callout_remaining=10000i,concurrent_sync_report_runs_remaining=20i,daily_bulk_api_requests_max=10000i,data_storage_mb_max=1073i,hourly_dashboard_statuses_remaining=999999999i,concurrent_async_get_report_instances_remaining=200i,daily_async_apex_executions_max=250000i,durable_streaming_api_concurrent_clients_max=2000i,file_storage_mb_max=1073i 1501565661000000000 +``` diff --git a/plugins/inputs/salesforce/salesforce.go b/plugins/inputs/salesforce/salesforce.go new file mode 100644 index 000000000..096550db5 --- /dev/null +++ b/plugins/inputs/salesforce/salesforce.go @@ -0,0 +1,245 @@ +package salesforce + +import ( + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/internal" + "github.com/influxdata/telegraf/plugins/inputs" +) + +var sampleConfig = ` + ## specify your credentials + ## + username = "your_username" + password = "your_password" + ## + ## (optional) security token + # security_token = "your_security_token" + ## + ## (optional) environment type (sandbox or production) + ## default is: production + ## + # environment = "production" + ## + ## (optional) API version (default: "39.0") + ## + # version = "39.0" +` + +type limit struct { + Max int + Remaining int +} + +type limits map[string]limit + +type Salesforce struct { + Username string + Password string + SecurityToken string + Environment string + SessionID string + ServerURL *url.URL + OrganizationID string + Version string + + client *http.Client +} + +const defaultVersion = "39.0" +const defaultEnvironment = "production" + +// returns a new Salesforce plugin instance +func NewSalesforce() *Salesforce { + tr := &http.Transport{ + ResponseHeaderTimeout: time.Duration(5 * time.Second), + } + client := &http.Client{ + Transport: tr, + Timeout: time.Duration(10 * time.Second), + } + return &Salesforce{ + client: client, + Version: defaultVersion, + Environment: defaultEnvironment} +} + +func (s *Salesforce) SampleConfig() string { + return sampleConfig +} + +func (s *Salesforce) Description() string { + return "Read API usage and limits for a Salesforce organisation" +} + +// Reads limits values from Salesforce API +func (s *Salesforce) Gather(acc telegraf.Accumulator) error { + limits, err := s.fetchLimits() + if err != nil { + return err + } + + tags := map[string]string{ + "organization_id": s.OrganizationID, + "host": s.ServerURL.Host, + } + + fields := make(map[string]interface{}) + for k, v := range limits { + key := internal.SnakeCase(k) + fields[key+"_max"] = v.Max + fields[key+"_remaining"] = v.Remaining + } + + acc.AddFields("salesforce", fields, tags) + return nil +} + +// query the limits endpoint +func (s *Salesforce) queryLimits() (*http.Response, error) { + endpoint := fmt.Sprintf("%s://%s/services/data/v%s/limits", s.ServerURL.Scheme, s.ServerURL.Host, s.Version) + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Add("Accept", "encoding/json") + req.Header.Add("Authorization", "Bearer "+s.SessionID) + return s.client.Do(req) +} + +func (s *Salesforce) isAuthenticated() bool { + return s.SessionID != "" +} + +func (s *Salesforce) fetchLimits() (limits, error) { + var l limits + if !s.isAuthenticated() { + if err := s.login(); err != nil { + return l, err + } + } + + resp, err := s.queryLimits() + if err != nil { + return l, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusUnauthorized { + if err = s.login(); err != nil { + return l, err + } + resp, err = s.queryLimits() + if err != nil { + return l, err + } + defer resp.Body.Close() + } + + if resp.StatusCode != http.StatusOK { + return l, fmt.Errorf("Salesforce responded with unexpected status code %d", resp.StatusCode) + } + + l = limits{} + err = json.NewDecoder(resp.Body).Decode(&l) + return l, err +} + +func (s *Salesforce) getLoginEndpoint() (string, error) { + switch s.Environment { + case "sandbox": + return fmt.Sprintf("https://test.salesforce.com/services/Soap/c/%s/", s.Version), nil + case "production": + return fmt.Sprintf("https://login.salesforce.com/services/Soap/c/%s/", s.Version), nil + default: + return "", fmt.Errorf("unknown environment type: %s", s.Environment) + } +} + +// Authenticate with Salesfroce +func (s *Salesforce) login() error { + if s.Username == "" || s.Password == "" { + return errors.New("missing username or password") + } + + body := fmt.Sprintf(` + + + + %s + %s%s + + + `, + s.Username, s.Password, s.SecurityToken) + + loginEndpoint, err := s.getLoginEndpoint() + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPost, loginEndpoint, strings.NewReader(body)) + if err != nil { + return err + } + req.Header.Add("Content-Type", "text/xml") + req.Header.Add("SOAPAction", "login") + + resp, err := s.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + soapFault := struct { + Code string `xml:"Body>Fault>faultcode"` + Message string `xml:"Body>Fault>faultstring"` + }{} + + err = xml.Unmarshal(respBody, &soapFault) + if err != nil { + return err + } + + if soapFault.Code != "" { + return fmt.Errorf("login failed: %s", soapFault.Message) + } + + loginResult := struct { + ServerURL string `xml:"Body>loginResponse>result>serverUrl"` + SessionID string `xml:"Body>loginResponse>result>sessionId"` + OrganizationID string `xml:"Body>loginResponse>result>userInfo>organizationId"` + }{} + + err = xml.Unmarshal(respBody, &loginResult) + if err != nil { + return err + } + + s.SessionID = loginResult.SessionID + s.OrganizationID = loginResult.OrganizationID + s.ServerURL, err = url.Parse(loginResult.ServerURL) + + return err +} + +func init() { + inputs.Add("salesforce", func() telegraf.Input { + return NewSalesforce() + }) +} diff --git a/plugins/inputs/salesforce/salesforce_test.go b/plugins/inputs/salesforce/salesforce_test.go new file mode 100644 index 000000000..288cc0f40 --- /dev/null +++ b/plugins/inputs/salesforce/salesforce_test.go @@ -0,0 +1,515 @@ +package salesforce_test + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/influxdata/telegraf/plugins/inputs/salesforce" + "github.com/influxdata/telegraf/testutil" + "github.com/stretchr/testify/require" +) + +func Test_Gather(t *testing.T) { + fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + _, _ = w.Write([]byte(testJson)) + })) + defer fakeServer.Close() + + plugin := salesforce.NewSalesforce() + plugin.SessionID = "test_session" + u, err := url.Parse(fakeServer.URL) + if err != nil { + t.Error(err) + } + plugin.ServerURL = u + + var acc testutil.Accumulator + require.NoError(t, acc.GatherError(plugin.Gather)) + + require.Len(t, acc.Metrics, 1) + m := acc.Metrics[0] + require.Len(t, m.Fields, 46) + require.Len(t, m.Tags, 2) +} + +var testJson = `{ + "ConcurrentAsyncGetReportInstances" : { + "Max" : 200, + "Remaining" : 200 + }, + "ConcurrentSyncReportRuns" : { + "Max" : 20, + "Remaining" : 20 + }, + "DailyApiRequests" : { + "Max" : 25000, + "Remaining" : 24926, + "AgilePoint" : { + "Max" : 0, + "Remaining" : 0 + }, + "Ant Migration Tool" : { + "Max" : 0, + "Remaining" : 0 + }, + "Axsy Server Integration" : { + "Max" : 0, + "Remaining" : 0 + }, + "Chatter Desktop" : { + "Max" : 0, + "Remaining" : 0 + }, + "Chatter Mobile for BlackBerry" : { + "Max" : 0, + "Remaining" : 0 + }, + "Dataloader Bulk" : { + "Max" : 0, + "Remaining" : 0 + }, + "Dataloader Partner" : { + "Max" : 0, + "Remaining" : 0 + }, + "EAHelperBot" : { + "Max" : 0, + "Remaining" : 0 + }, + "Force.com IDE" : { + "Max" : 0, + "Remaining" : 0 + }, + "LiveText for Salesforce" : { + "Max" : 0, + "Remaining" : 0 + }, + "LiveText for Salesforce (QA)" : { + "Max" : 0, + "Remaining" : 0 + }, + "MyU App" : { + "Max" : 0, + "Remaining" : 0 + }, + "SMS Magic Interact" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Chatter" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Files" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Mobile Dashboards" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Social Customer Service (SCS)" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Touch" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce for Outlook" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce1 for Android" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce1 for iOS" : { + "Max" : 0, + "Remaining" : 0 + }, + "SalesforceA" : { + "Max" : 0, + "Remaining" : 0 + }, + "SalesforceIQ" : { + "Max" : 0, + "Remaining" : 0 + }, + "Workbench" : { + "Max" : 0, + "Remaining" : 0 + } + }, + "DailyAsyncApexExecutions" : { + "Max" : 250000, + "Remaining" : 250000 + }, + "DailyBulkApiRequests" : { + "Max" : 10000, + "Remaining" : 10000, + "AgilePoint" : { + "Max" : 0, + "Remaining" : 0 + }, + "Ant Migration Tool" : { + "Max" : 0, + "Remaining" : 0 + }, + "Axsy Server Integration" : { + "Max" : 0, + "Remaining" : 0 + }, + "Chatter Desktop" : { + "Max" : 0, + "Remaining" : 0 + }, + "Chatter Mobile for BlackBerry" : { + "Max" : 0, + "Remaining" : 0 + }, + "Dataloader Bulk" : { + "Max" : 0, + "Remaining" : 0 + }, + "Dataloader Partner" : { + "Max" : 0, + "Remaining" : 0 + }, + "EAHelperBot" : { + "Max" : 0, + "Remaining" : 0 + }, + "Force.com IDE" : { + "Max" : 0, + "Remaining" : 0 + }, + "LiveText for Salesforce" : { + "Max" : 0, + "Remaining" : 0 + }, + "LiveText for Salesforce (QA)" : { + "Max" : 0, + "Remaining" : 0 + }, + "MyU App" : { + "Max" : 0, + "Remaining" : 0 + }, + "SMS Magic Interact" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Chatter" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Files" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Mobile Dashboards" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Social Customer Service (SCS)" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Touch" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce for Outlook" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce1 for Android" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce1 for iOS" : { + "Max" : 0, + "Remaining" : 0 + }, + "SalesforceA" : { + "Max" : 0, + "Remaining" : 0 + }, + "SalesforceIQ" : { + "Max" : 0, + "Remaining" : 0 + }, + "Workbench" : { + "Max" : 0, + "Remaining" : 0 + } + }, + "DailyDurableGenericStreamingApiEvents" : { + "Max" : 10000, + "Remaining" : 10000 + }, + "DailyDurableStreamingApiEvents" : { + "Max" : 10000, + "Remaining" : 10000 + }, + "DailyGenericStreamingApiEvents" : { + "Max" : 10000, + "Remaining" : 10000, + "AgilePoint" : { + "Max" : 0, + "Remaining" : 0 + }, + "Ant Migration Tool" : { + "Max" : 0, + "Remaining" : 0 + }, + "Axsy Server Integration" : { + "Max" : 0, + "Remaining" : 0 + }, + "Chatter Desktop" : { + "Max" : 0, + "Remaining" : 0 + }, + "Chatter Mobile for BlackBerry" : { + "Max" : 0, + "Remaining" : 0 + }, + "Dataloader Bulk" : { + "Max" : 0, + "Remaining" : 0 + }, + "Dataloader Partner" : { + "Max" : 0, + "Remaining" : 0 + }, + "EAHelperBot" : { + "Max" : 0, + "Remaining" : 0 + }, + "Force.com IDE" : { + "Max" : 0, + "Remaining" : 0 + }, + "LiveText for Salesforce" : { + "Max" : 0, + "Remaining" : 0 + }, + "LiveText for Salesforce (QA)" : { + "Max" : 0, + "Remaining" : 0 + }, + "MyU App" : { + "Max" : 0, + "Remaining" : 0 + }, + "SMS Magic Interact" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Chatter" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Files" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Mobile Dashboards" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Social Customer Service (SCS)" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Touch" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce for Outlook" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce1 for Android" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce1 for iOS" : { + "Max" : 0, + "Remaining" : 0 + }, + "SalesforceA" : { + "Max" : 0, + "Remaining" : 0 + }, + "SalesforceIQ" : { + "Max" : 0, + "Remaining" : 0 + }, + "Workbench" : { + "Max" : 0, + "Remaining" : 0 + } + }, + "DailyStreamingApiEvents" : { + "Max" : 20000, + "Remaining" : 20000, + "AgilePoint" : { + "Max" : 0, + "Remaining" : 0 + }, + "Ant Migration Tool" : { + "Max" : 0, + "Remaining" : 0 + }, + "Axsy Server Integration" : { + "Max" : 0, + "Remaining" : 0 + }, + "Chatter Desktop" : { + "Max" : 0, + "Remaining" : 0 + }, + "Chatter Mobile for BlackBerry" : { + "Max" : 0, + "Remaining" : 0 + }, + "Dataloader Bulk" : { + "Max" : 0, + "Remaining" : 0 + }, + "Dataloader Partner" : { + "Max" : 0, + "Remaining" : 0 + }, + "EAHelperBot" : { + "Max" : 0, + "Remaining" : 0 + }, + "Force.com IDE" : { + "Max" : 0, + "Remaining" : 0 + }, + "LiveText for Salesforce" : { + "Max" : 0, + "Remaining" : 0 + }, + "LiveText for Salesforce (QA)" : { + "Max" : 0, + "Remaining" : 0 + }, + "MyU App" : { + "Max" : 0, + "Remaining" : 0 + }, + "SMS Magic Interact" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Chatter" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Files" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Mobile Dashboards" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Social Customer Service (SCS)" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce Touch" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce for Outlook" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce1 for Android" : { + "Max" : 0, + "Remaining" : 0 + }, + "Salesforce1 for iOS" : { + "Max" : 0, + "Remaining" : 0 + }, + "SalesforceA" : { + "Max" : 0, + "Remaining" : 0 + }, + "SalesforceIQ" : { + "Max" : 0, + "Remaining" : 0 + }, + "Workbench" : { + "Max" : 0, + "Remaining" : 0 + } + }, + "DailyWorkflowEmails" : { + "Max" : 20000, + "Remaining" : 20000 + }, + "DataStorageMB" : { + "Max" : 209, + "Remaining" : 207 + }, + "DurableStreamingApiConcurrentClients" : { + "Max" : 20, + "Remaining" : 20 + }, + "FileStorageMB" : { + "Max" : 209, + "Remaining" : 206 + }, + "HourlyAsyncReportRuns" : { + "Max" : 1200, + "Remaining" : 1200 + }, + "HourlyDashboardRefreshes" : { + "Max" : 200, + "Remaining" : 200 + }, + "HourlyDashboardResults" : { + "Max" : 5000, + "Remaining" : 5000 + }, + "HourlyDashboardStatuses" : { + "Max" : 999999999, + "Remaining" : 999999999 + }, + "HourlyODataCallout" : { + "Max" : 20000, + "Remaining" : 19998 + }, + "HourlySyncReportRuns" : { + "Max" : 500, + "Remaining" : 500 + }, + "HourlyTimeBasedWorkflow" : { + "Max" : 50, + "Remaining" : 50 + }, + "MassEmail" : { + "Max" : 5000, + "Remaining" : 5000 + }, + "SingleEmail" : { + "Max" : 5000, + "Remaining" : 5000 + }, + "StreamingApiConcurrentClients" : { + "Max" : 20, + "Remaining" : 20 + } +}`