Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new groundwork output plugin #9891

Merged
merged 13 commits into from
Nov 30, 2021
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")
}
Comment on lines +68 to +76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just set the default values here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We made that to avoid case when user sets empty string. User's value should override value from init() but still can be empty string

Copy link
Member

@srebhan srebhan Nov 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Other plugins handle empty strings set by the user just as if they have set the default. In this case, you can move the default handling in here and just use the default if the string is empty, no matter if not set or explicitly set empty by the user.
This makes the code a bit cleaner, but if you insist on the present behavior, you should at least add a comment on why you do so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was like that before, but then it got changed on my request. Actually, I only requested it to be done for DefaultServiceState, not the others. So @VladislavSenkevich I'm fine with whatever will come out if @srebhan is fine with it as well.


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