Skip to content

Commit

Permalink
feat: add new groundwork output plugin (influxdata#9891)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vladislav authored Nov 30, 2021
1 parent d9eb4d0 commit 27dea9b
Show file tree
Hide file tree
Showing 7 changed files with 429 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions plugins/outputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
38 changes: 38 additions & 0 deletions plugins/outputs/groundwork/README.md
Original file line number Diff line number Diff line change
@@ -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.
289 changes: 289 additions & 0 deletions plugins/outputs/groundwork/groundwork.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 27dea9b

Please sign in to comment.