feat: add new groundwork output plugin (#9891)
This commit is contained in:
parent
d9eb4d06c5
commit
27dea9bd8f
|
|
@ -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)
|
||||
|
|
|
|||
3
go.mod
3
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
Loading…
Reference in New Issue