diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index e1caaf320..f9e46d94c 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -115,6 +115,7 @@ following works: - github.com/gosnmp/gosnmp [BSD 2-Clause "Simplified" License](https://github.com/gosnmp/gosnmp/blob/master/LICENSE) - github.com/grid-x/modbus [BSD 3-Clause "New" or "Revised" License](https://github.com/grid-x/modbus/blob/master/LICENSE) - github.com/grid-x/serial [MIT License](https://github.com/grid-x/serial/blob/master/LICENSE) +- github.com/gwos/tcg/sdk [MIT License](https://github.com/gwos/tcg/blob/master/LICENSE) - github.com/hailocab/go-hostpool [MIT License](https://github.com/hailocab/go-hostpool/blob/master/LICENSE) - github.com/harlow/kinesis-consumer [MIT License](https://github.com/harlow/kinesis-consumer/blob/master/MIT-LICENSE) - github.com/hashicorp/consul/api [Mozilla Public License 2.0](https://github.com/hashicorp/consul/blob/master/LICENSE) diff --git a/go.mod b/go.mod index 2caf8e9ae..1429f31fd 100644 --- a/go.mod +++ b/go.mod @@ -133,6 +133,7 @@ require ( github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/gwos/tcg/sdk v0.0.0-20211130162655-32ad77586ccf github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/harlow/kinesis-consumer v0.3.6-0.20210911031324-5a873d6e9fec github.com/hashicorp/consul/api v1.9.1 @@ -141,7 +142,7 @@ require ( github.com/hashicorp/go-immutable-radix v1.2.0 // indirect github.com/hashicorp/go-msgpack v0.5.5 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.2 github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/serf v0.9.5 // indirect github.com/influxdata/go-syslog/v3 v3.0.0 diff --git a/go.sum b/go.sum index 727dc6e6d..afda01794 100644 --- a/go.sum +++ b/go.sum @@ -1145,6 +1145,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.14.5/go.mod h1:UJ0EZAp832vCd54Wev9N1BM github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= +github.com/gwos/tcg/sdk v0.0.0-20211130162655-32ad77586ccf h1:xSjgqa6SiBaSC4sTC4HniWRLww2vbl3u0KyMUYeryJI= +github.com/gwos/tcg/sdk v0.0.0-20211130162655-32ad77586ccf/go.mod h1:OjlJNRXwlEjznVfU3YtLWH8FyM7KWHUevXDI47UeZeM= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/harlow/kinesis-consumer v0.3.6-0.20210911031324-5a873d6e9fec h1:ya+kv1eNnd5QhcHuaj5g5eMq5Ra3VCNaPY2ZI7Aq91o= diff --git a/plugins/outputs/all/all.go b/plugins/outputs/all/all.go index ff3f2251a..ff23a060b 100644 --- a/plugins/outputs/all/all.go +++ b/plugins/outputs/all/all.go @@ -21,6 +21,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/outputs/file" _ "github.com/influxdata/telegraf/plugins/outputs/graphite" _ "github.com/influxdata/telegraf/plugins/outputs/graylog" + _ "github.com/influxdata/telegraf/plugins/outputs/groundwork" _ "github.com/influxdata/telegraf/plugins/outputs/health" _ "github.com/influxdata/telegraf/plugins/outputs/http" _ "github.com/influxdata/telegraf/plugins/outputs/influxdb" diff --git a/plugins/outputs/groundwork/README.md b/plugins/outputs/groundwork/README.md new file mode 100644 index 000000000..ea0fc92fc --- /dev/null +++ b/plugins/outputs/groundwork/README.md @@ -0,0 +1,38 @@ +# GroundWork Output Plugin + +This plugin writes to a [GroundWork Monitor][1] instance. Plugin only supports GW8+ + +[1]: https://www.gwos.com/product/groundwork-monitor/ + +## Configuration + +```toml +[[outputs.groundwork]] + ## URL of your groundwork instance. + url = "https://groundwork.example.com" + + ## Agent uuid for GroundWork API Server. + agent_id = "" + + ## Username and password to access GroundWork API. + username = "" + password = "" + + ## Default display name for the host with services(metrics). + # default_host = "telegraf" + + ## Default service state. + # default_service_state = "SERVICE_OK" + + ## The name of the tag that contains the hostname. + # resource_tag = "host" +``` + +## List of tags used by the plugin + +* service - to define the name of the service you want to monitor. +* status - to define the status of the service. +* message - to provide any message you want. +* unitType - to use in monitoring contexts(subset of The Unified Code for Units of Measure standard). Supported types: "1", "%cpu", "KB", "GB", "MB". +* warning - to define warning threshold value. +* critical - to define critical threshold value. diff --git a/plugins/outputs/groundwork/groundwork.go b/plugins/outputs/groundwork/groundwork.go new file mode 100644 index 000000000..ec11439b8 --- /dev/null +++ b/plugins/outputs/groundwork/groundwork.go @@ -0,0 +1,289 @@ +package groundwork + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + + "github.com/gwos/tcg/sdk/clients" + "github.com/gwos/tcg/sdk/transit" + "github.com/hashicorp/go-uuid" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/outputs" +) + +const sampleConfig = ` + ## URL of your groundwork instance. + url = "https://groundwork.example.com" + + ## Agent uuid for GroundWork API Server. + agent_id = "" + + ## Username and password to access GroundWork API. + username = "" + password = "" + + ## Default display name for the host with services(metrics). + # default_host = "telegraf" + + ## Default service state. + # default_service_state = "SERVICE_OK" + + ## The name of the tag that contains the hostname. + # resource_tag = "host" +` + +type Groundwork struct { + Server string `toml:"url"` + AgentID string `toml:"agent_id"` + Username string `toml:"username"` + Password string `toml:"password"` + DefaultHost string `toml:"default_host"` + DefaultServiceState string `toml:"default_service_state"` + ResourceTag string `toml:"resource_tag"` + Log telegraf.Logger `toml:"-"` + client clients.GWClient +} + +func (g *Groundwork) SampleConfig() string { + return sampleConfig +} + +func (g *Groundwork) Init() error { + if g.Server == "" { + return errors.New("no 'url' provided") + } + if g.AgentID == "" { + return errors.New("no 'agent_id' provided") + } + if g.Username == "" { + return errors.New("no 'username' provided") + } + if g.Password == "" { + return errors.New("no 'password' provided") + } + if g.DefaultHost == "" { + return errors.New("no 'default_host' provided") + } + if g.ResourceTag == "" { + return errors.New("no 'resource_tag' provided") + } + if !validStatus(g.DefaultServiceState) { + return errors.New("invalid 'default_service_state' provided") + } + + g.client = clients.GWClient{ + AppName: "telegraf", + AppType: "TELEGRAF", + GWConnection: &clients.GWConnection{ + HostName: g.Server, + UserName: g.Username, + Password: g.Password, + IsDynamicInventory: true, + }, + } + return nil +} + +func (g *Groundwork) Connect() error { + err := g.client.Connect() + if err != nil { + return fmt.Errorf("could not log in: %v", err) + } + return nil +} + +func (g *Groundwork) Close() error { + err := g.client.Disconnect() + if err != nil { + return fmt.Errorf("could not log out: %v", err) + } + return nil +} + +func (g *Groundwork) Write(metrics []telegraf.Metric) error { + resourceToServicesMap := make(map[string][]transit.DynamicMonitoredService) + for _, metric := range metrics { + resource, service, err := g.parseMetric(metric) + if err != nil { + g.Log.Errorf("%v", err) + continue + } + resourceToServicesMap[resource] = append(resourceToServicesMap[resource], *service) + } + + var resources []transit.DynamicMonitoredResource + for resourceName, services := range resourceToServicesMap { + resources = append(resources, transit.DynamicMonitoredResource{ + BaseResource: transit.BaseResource{ + BaseTransitData: transit.BaseTransitData{ + Name: resourceName, + Type: transit.Host, + }, + }, + Status: transit.HostUp, + LastCheckTime: transit.NewTimestamp(), + Services: services, + }) + } + + traceToken, err := uuid.GenerateUUID() + if err != nil { + return err + } + requestJSON, err := json.Marshal(transit.DynamicResourcesWithServicesRequest{ + Context: &transit.TracerContext{ + AppType: "TELEGRAF", + AgentID: g.AgentID, + TraceToken: traceToken, + TimeStamp: transit.NewTimestamp(), + Version: transit.ModelVersion, + }, + Resources: resources, + Groups: nil, + }) + + if err != nil { + return err + } + + _, err = g.client.SendResourcesWithMetrics(context.Background(), requestJSON) + if err != nil { + return fmt.Errorf("error while sending: %v", err) + } + + return nil +} + +func (g *Groundwork) Description() string { + return "Send telegraf metrics to GroundWork Monitor" +} + +func init() { + outputs.Add("groundwork", func() telegraf.Output { + return &Groundwork{ + ResourceTag: "host", + DefaultHost: "telegraf", + DefaultServiceState: string(transit.ServiceOk), + } + }) +} + +func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, *transit.DynamicMonitoredService, error) { + resource := g.DefaultHost + if value, present := metric.GetTag(g.ResourceTag); present { + resource = value + } + + service := metric.Name() + if value, present := metric.GetTag("service"); present { + service = value + } + + status := g.DefaultServiceState + value, statusPresent := metric.GetTag("status") + if validStatus(value) { + status = value + } + + message, _ := metric.GetTag("message") + + unitType := string(transit.UnitCounter) + if value, present := metric.GetTag("unitType"); present { + unitType = value + } + + var critical float64 + value, criticalPresent := metric.GetTag("critical") + if criticalPresent { + if s, err := strconv.ParseFloat(value, 64); err == nil { + critical = s + } + } + + var warning float64 + value, warningPresent := metric.GetTag("warning") + if warningPresent { + if s, err := strconv.ParseFloat(value, 64); err == nil { + warning = s + } + } + + lastCheckTime := transit.NewTimestamp() + lastCheckTime.Time = metric.Time() + serviceObject := transit.DynamicMonitoredService{ + BaseTransitData: transit.BaseTransitData{ + Name: service, + Type: transit.Service, + Owner: resource, + }, + Status: transit.MonitorStatus(status), + LastCheckTime: lastCheckTime, + LastPlugInOutput: message, + Metrics: nil, + } + + for _, value := range metric.FieldList() { + var thresholds []transit.ThresholdValue + if warningPresent { + thresholds = append(thresholds, transit.ThresholdValue{ + SampleType: transit.Warning, + Label: value.Key + "_wn", + Value: &transit.TypedValue{ + ValueType: transit.DoubleType, + DoubleValue: warning, + }, + }) + } + if criticalPresent { + thresholds = append(thresholds, transit.ThresholdValue{ + SampleType: transit.Critical, + Label: value.Key + "_cr", + Value: &transit.TypedValue{ + ValueType: transit.DoubleType, + DoubleValue: critical, + }, + }) + } + + typedValue := new(transit.TypedValue) + err := typedValue.FromInterface(value.Value) + if err != nil { + return "", nil, err + } + + serviceObject.Metrics = append(serviceObject.Metrics, transit.TimeSeries{ + MetricName: value.Key, + SampleType: transit.Value, + Interval: &transit.TimeInterval{ + EndTime: lastCheckTime, + }, + Value: typedValue, + Unit: transit.UnitType(unitType), + Thresholds: &thresholds, + }) + } + + if !statusPresent { + serviceStatus, err := transit.CalculateServiceStatus(&serviceObject.Metrics) + if err != nil { + g.Log.Infof("could not calculate service status, reverting to default_service_state: %v", err) + serviceObject.Status = transit.MonitorStatus(g.DefaultServiceState) + } + serviceObject.Status = serviceStatus + } + + return resource, &serviceObject, nil +} + +func validStatus(status string) bool { + switch transit.MonitorStatus(status) { + case transit.ServiceOk, transit.ServiceWarning, transit.ServicePending, transit.ServiceScheduledCritical, + transit.ServiceUnscheduledCritical, transit.ServiceUnknown: + return true + } + return false +} diff --git a/plugins/outputs/groundwork/groundwork_test.go b/plugins/outputs/groundwork/groundwork_test.go new file mode 100644 index 000000000..16ae1f057 --- /dev/null +++ b/plugins/outputs/groundwork/groundwork_test.go @@ -0,0 +1,96 @@ +package groundwork + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gwos/tcg/sdk/clients" + "github.com/stretchr/testify/require" + + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/testutil" +) + +const ( + defaultTestAgentID = "ec1676cc-583d-48ee-b035-7fb5ed0fcf88" + defaultHost = "telegraf" +) + +func TestWrite(t *testing.T) { + // Generate test metric with default name to test Write logic + floatMetric := testutil.TestMetric(1.0, "Float") + stringMetric := testutil.TestMetric("Test", "String") + + // Simulate Groundwork server that should receive custom metrics + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + + // Decode body to use in assertations below + var obj groundworkObject + err = json.Unmarshal(body, &obj) + require.NoError(t, err) + + // Check if server gets valid metrics object + require.Equal(t, obj.Context.AgentID, defaultTestAgentID) + require.Equal(t, obj.Resources[0].Name, defaultHost) + require.Equal( + t, + obj.Resources[0].Services[0].Name, + "Float", + ) + require.Equal( + t, + obj.Resources[0].Services[0].Metrics[0].Value.DoubleValue, + 1.0, + ) + require.Equal( + t, + obj.Resources[0].Services[1].Metrics[0].Value.StringValue, + "Test", + ) + + _, err = fmt.Fprintln(w, `OK`) + require.NoError(t, err) + })) + + i := Groundwork{ + Server: server.URL, + AgentID: defaultTestAgentID, + DefaultHost: "telegraf", + client: clients.GWClient{ + AppName: "telegraf", + AppType: "TELEGRAF", + GWConnection: &clients.GWConnection{ + HostName: server.URL, + }, + }, + } + + err := i.Write([]telegraf.Metric{floatMetric, stringMetric}) + require.NoError(t, err) + + defer server.Close() +} + +type groundworkObject struct { + Context struct { + AgentID string `json:"agentId"` + } `json:"context"` + Resources []struct { + Name string `json:"name"` + Services []struct { + Name string `json:"name"` + Metrics []struct { + Value struct { + StringValue string `json:"stringValue"` + DoubleValue float64 `json:"doubleValue"` + } `json:"value"` + } + } `json:"services"` + } `json:"resources"` +}