Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
92
plugins/outputs/groundwork/README.md
Normal file
92
plugins/outputs/groundwork/README.md
Normal file
|
@ -0,0 +1,92 @@
|
|||
# GroundWork Output Plugin
|
||||
|
||||
This plugin writes metrics to a [GroundWork Monitor][groundwork] instance.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Plugin only supports GroundWork v8 or later.
|
||||
|
||||
⭐ Telegraf v1.21.0
|
||||
🏷️ applications, messaging
|
||||
💻 all
|
||||
|
||||
[groundwork]: https://www.gwos.com/product/groundwork-monitor/
|
||||
|
||||
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
|
||||
|
||||
In addition to the plugin-specific configuration settings, plugins support
|
||||
additional global and plugin configuration settings. These settings are used to
|
||||
modify metrics, tags, and field or create aliases and configure ordering, etc.
|
||||
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||
|
||||
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
|
||||
|
||||
## Secret-store support
|
||||
|
||||
This plugin supports secrets from secret-stores for the `username` and
|
||||
`password` option.
|
||||
See the [secret-store documentation][SECRETSTORE] for more details on how
|
||||
to use them.
|
||||
|
||||
[SECRETSTORE]: ../../../docs/CONFIGURATION.md#secret-store-secrets
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml @sample.conf
|
||||
# Send telegraf metrics to GroundWork Monitor
|
||||
[[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 application type to use in GroundWork client
|
||||
# default_app_type = "TELEGRAF"
|
||||
|
||||
## 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"
|
||||
|
||||
## The name of the tag that contains the host group name.
|
||||
# group_tag = "group"
|
||||
```
|
||||
|
||||
## List of tags used by the plugin
|
||||
|
||||
* __group__ - to define the name of the group you want to monitor,
|
||||
can be changed with config.
|
||||
* __host__ - to define the name of the host you want to monitor,
|
||||
can be changed with config.
|
||||
* __service__ - to define the name of the service you want to monitor.
|
||||
* __status__ - to define the status of the service. Supported statuses:
|
||||
"SERVICE_OK", "SERVICE_WARNING", "SERVICE_UNSCHEDULED_CRITICAL",
|
||||
"SERVICE_PENDING", "SERVICE_SCHEDULED_CRITICAL", "SERVICE_UNKNOWN".
|
||||
* __message__ - to provide any message you want,
|
||||
it overrides __message__ field value.
|
||||
* __unitType__ - to use in monitoring contexts (subset of The Unified Code for
|
||||
Units of Measure standard). Supported types: "1", "%cpu", "KB", "GB", "MB".
|
||||
* __critical__ - to define the default critical threshold value,
|
||||
it overrides value_cr field value.
|
||||
* __warning__ - to define the default warning threshold value,
|
||||
it overrides value_wn field value.
|
||||
* __value_cr__ - to define critical threshold value,
|
||||
it overrides __critical__ tag value and __value_cr__ field value.
|
||||
* __value_wn__ - to define warning threshold value,
|
||||
it overrides __warning__ tag value and __value_wn__ field value.
|
||||
|
||||
## NOTE
|
||||
|
||||
The current version of GroundWork Monitor does not support metrics whose values
|
||||
are strings. Such metrics will be skipped and will not be added to the final
|
||||
payload. You can find more context in this pull request: [#10255][].
|
||||
|
||||
[#10255]: https://github.com/influxdata/telegraf/pull/10255
|
384
plugins/outputs/groundwork/groundwork.go
Normal file
384
plugins/outputs/groundwork/groundwork.go
Normal file
|
@ -0,0 +1,384 @@
|
|||
//go:generate ../../../tools/readme_config_includer/generator
|
||||
package groundwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gwos/tcg/sdk/clients"
|
||||
"github.com/gwos/tcg/sdk/log"
|
||||
"github.com/gwos/tcg/sdk/transit"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/plugins/outputs"
|
||||
)
|
||||
|
||||
//go:embed sample.conf
|
||||
var sampleConfig string
|
||||
|
||||
type metricMeta struct {
|
||||
group string
|
||||
resource string
|
||||
}
|
||||
|
||||
type Groundwork struct {
|
||||
Server string `toml:"url"`
|
||||
AgentID string `toml:"agent_id"`
|
||||
Username config.Secret `toml:"username"`
|
||||
Password config.Secret `toml:"password"`
|
||||
DefaultAppType string `toml:"default_app_type"`
|
||||
DefaultHost string `toml:"default_host"`
|
||||
DefaultServiceState string `toml:"default_service_state"`
|
||||
GroupTag string `toml:"group_tag"`
|
||||
ResourceTag string `toml:"resource_tag"`
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
client clients.GWClient
|
||||
}
|
||||
|
||||
func (*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.Empty() {
|
||||
return errors.New(`no "username" provided`)
|
||||
}
|
||||
if g.Password.Empty() {
|
||||
return errors.New(`no "password" provided`)
|
||||
}
|
||||
if g.DefaultAppType == "" {
|
||||
return errors.New(`no "default_app_type" 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`)
|
||||
}
|
||||
|
||||
username, err := g.Username.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting username failed: %w", err)
|
||||
}
|
||||
password, err := g.Password.Get()
|
||||
if err != nil {
|
||||
username.Destroy()
|
||||
return fmt.Errorf("getting password failed: %w", err)
|
||||
}
|
||||
g.client = clients.GWClient{
|
||||
AppName: "telegraf",
|
||||
AppType: g.DefaultAppType,
|
||||
GWConnection: &clients.GWConnection{
|
||||
HostName: g.Server,
|
||||
UserName: username.String(),
|
||||
Password: password.String(),
|
||||
IsDynamicInventory: true,
|
||||
},
|
||||
}
|
||||
username.Destroy()
|
||||
password.Destroy()
|
||||
|
||||
/* adapt SDK logger */
|
||||
log.Logger = newLogger(g.Log).WithGroup("tcg.sdk")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Groundwork) Connect() error {
|
||||
err := g.client.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not login: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Groundwork) Close() error {
|
||||
err := g.client.Disconnect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not logout: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Groundwork) Write(metrics []telegraf.Metric) error {
|
||||
groupMap := make(map[string][]transit.ResourceRef)
|
||||
resourceToServicesMap := make(map[string][]transit.MonitoredService)
|
||||
for _, metric := range metrics {
|
||||
meta, service := g.parseMetric(metric)
|
||||
resource := meta.resource
|
||||
resourceToServicesMap[resource] = append(resourceToServicesMap[resource], *service)
|
||||
|
||||
group := meta.group
|
||||
if len(group) != 0 {
|
||||
resRef := transit.ResourceRef{
|
||||
Name: resource,
|
||||
Type: transit.ResourceTypeHost,
|
||||
}
|
||||
if refs, ok := groupMap[group]; ok {
|
||||
refs = append(refs, resRef)
|
||||
groupMap[group] = refs
|
||||
} else {
|
||||
groupMap[group] = []transit.ResourceRef{resRef}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([]transit.ResourceGroup, 0, len(groupMap))
|
||||
for groupName, refs := range groupMap {
|
||||
groups = append(groups, transit.ResourceGroup{
|
||||
GroupName: groupName,
|
||||
Resources: refs,
|
||||
Type: transit.HostGroup,
|
||||
})
|
||||
}
|
||||
|
||||
resources := make([]transit.MonitoredResource, 0, len(resourceToServicesMap))
|
||||
for resourceName, services := range resourceToServicesMap {
|
||||
resources = append(resources, transit.MonitoredResource{
|
||||
BaseResource: transit.BaseResource{
|
||||
BaseInfo: transit.BaseInfo{
|
||||
Name: resourceName,
|
||||
Type: transit.ResourceTypeHost,
|
||||
},
|
||||
},
|
||||
MonitoredInfo: transit.MonitoredInfo{
|
||||
Status: transit.HostUp,
|
||||
LastCheckTime: transit.NewTimestamp(),
|
||||
},
|
||||
Services: services,
|
||||
})
|
||||
}
|
||||
|
||||
traceToken, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requestJSON, err := json.Marshal(transit.ResourcesWithServicesRequest{
|
||||
Context: &transit.TracerContext{
|
||||
AppType: g.DefaultAppType,
|
||||
AgentID: g.AgentID,
|
||||
TraceToken: traceToken,
|
||||
TimeStamp: transit.NewTimestamp(),
|
||||
Version: transit.ModelVersion,
|
||||
},
|
||||
Resources: resources,
|
||||
Groups: groups,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = g.client.SendResourcesWithMetrics(context.Background(), requestJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while sending: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
outputs.Add("groundwork", func() telegraf.Output {
|
||||
return &Groundwork{
|
||||
GroupTag: "group",
|
||||
ResourceTag: "host",
|
||||
DefaultHost: "telegraf",
|
||||
DefaultAppType: "TELEGRAF",
|
||||
DefaultServiceState: string(transit.ServiceOk),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Groundwork) parseMetric(metric telegraf.Metric) (metricMeta, *transit.MonitoredService) {
|
||||
group, _ := metric.GetTag(g.GroupTag)
|
||||
|
||||
resource := g.DefaultHost
|
||||
if v, ok := metric.GetTag(g.ResourceTag); ok {
|
||||
resource = v
|
||||
}
|
||||
|
||||
service := metric.Name()
|
||||
if v, ok := metric.GetTag("service"); ok {
|
||||
service = v
|
||||
}
|
||||
|
||||
unitType := string(transit.UnitCounter)
|
||||
if v, ok := metric.GetTag("unitType"); ok {
|
||||
unitType = v
|
||||
}
|
||||
|
||||
lastCheckTime := transit.NewTimestamp()
|
||||
lastCheckTime.Time = metric.Time()
|
||||
serviceObject := transit.MonitoredService{
|
||||
BaseInfo: transit.BaseInfo{
|
||||
Name: service,
|
||||
Type: transit.ResourceTypeService,
|
||||
Owner: resource,
|
||||
Properties: make(map[string]transit.TypedValue),
|
||||
},
|
||||
MonitoredInfo: transit.MonitoredInfo{
|
||||
Status: transit.MonitorStatus(g.DefaultServiceState),
|
||||
LastCheckTime: lastCheckTime,
|
||||
NextCheckTime: lastCheckTime, // if not added, GW will make this as LastCheckTime + 5 mins
|
||||
},
|
||||
Metrics: nil,
|
||||
}
|
||||
|
||||
knownKey := func(t string) bool {
|
||||
if strings.HasSuffix(t, "_cr") ||
|
||||
strings.HasSuffix(t, "_wn") ||
|
||||
t == "critical" ||
|
||||
t == "warning" ||
|
||||
t == g.GroupTag ||
|
||||
t == g.ResourceTag ||
|
||||
t == "service" ||
|
||||
t == "status" ||
|
||||
t == "message" ||
|
||||
t == "unitType" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, tag := range metric.TagList() {
|
||||
if knownKey(tag.Key) {
|
||||
continue
|
||||
}
|
||||
serviceObject.Properties[tag.Key] = *transit.NewTypedValue(tag.Value)
|
||||
}
|
||||
|
||||
for _, field := range metric.FieldList() {
|
||||
if knownKey(field.Key) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field.Value.(type) {
|
||||
case string, []byte:
|
||||
g.Log.Warnf("string values are not supported, skipping field %s: %q", field.Key, field.Value)
|
||||
continue
|
||||
}
|
||||
|
||||
typedValue := transit.NewTypedValue(field.Value)
|
||||
if typedValue == nil {
|
||||
g.Log.Warnf("could not convert type %T, skipping field %s: %v", field.Value, field.Key, field.Value)
|
||||
continue
|
||||
}
|
||||
|
||||
var thresholds []transit.ThresholdValue
|
||||
addCriticalThreshold := func(v interface{}) {
|
||||
if tv := transit.NewTypedValue(v); tv != nil {
|
||||
thresholds = append(thresholds, transit.ThresholdValue{
|
||||
SampleType: transit.Critical,
|
||||
Label: field.Key + "_cr",
|
||||
Value: tv,
|
||||
})
|
||||
}
|
||||
}
|
||||
addWarningThreshold := func(v interface{}) {
|
||||
if tv := transit.NewTypedValue(v); tv != nil {
|
||||
thresholds = append(thresholds, transit.ThresholdValue{
|
||||
SampleType: transit.Warning,
|
||||
Label: field.Key + "_wn",
|
||||
Value: tv,
|
||||
})
|
||||
}
|
||||
}
|
||||
if v, ok := metric.GetTag(field.Key + "_cr"); ok {
|
||||
if v, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
addCriticalThreshold(v)
|
||||
}
|
||||
} else if v, ok := metric.GetTag("critical"); ok {
|
||||
if v, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
addCriticalThreshold(v)
|
||||
}
|
||||
} else if v, ok := metric.GetField(field.Key + "_cr"); ok {
|
||||
addCriticalThreshold(v)
|
||||
}
|
||||
if v, ok := metric.GetTag(field.Key + "_wn"); ok {
|
||||
if v, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
addWarningThreshold(v)
|
||||
}
|
||||
} else if v, ok := metric.GetTag("warning"); ok {
|
||||
if v, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
addWarningThreshold(v)
|
||||
}
|
||||
} else if v, ok := metric.GetField(field.Key + "_wn"); ok {
|
||||
addWarningThreshold(v)
|
||||
}
|
||||
|
||||
serviceObject.Metrics = append(serviceObject.Metrics, transit.TimeSeries{
|
||||
MetricName: field.Key,
|
||||
SampleType: transit.Value,
|
||||
Interval: &transit.TimeInterval{EndTime: lastCheckTime},
|
||||
Value: typedValue,
|
||||
Unit: transit.UnitType(unitType),
|
||||
Thresholds: thresholds,
|
||||
})
|
||||
}
|
||||
|
||||
if m, ok := metric.GetTag("message"); ok {
|
||||
serviceObject.LastPluginOutput = strings.ToValidUTF8(m, "?")
|
||||
} else if m, ok := metric.GetField("message"); ok {
|
||||
switch m := m.(type) {
|
||||
case string:
|
||||
serviceObject.LastPluginOutput = strings.ToValidUTF8(m, "?")
|
||||
case []byte:
|
||||
serviceObject.LastPluginOutput = strings.ToValidUTF8(string(m), "?")
|
||||
default:
|
||||
serviceObject.LastPluginOutput = strings.ToValidUTF8(fmt.Sprintf("%v", m), "?")
|
||||
}
|
||||
}
|
||||
|
||||
func() {
|
||||
if s, ok := metric.GetTag("status"); ok && validStatus(s) {
|
||||
serviceObject.Status = transit.MonitorStatus(s)
|
||||
return
|
||||
}
|
||||
if s, ok := metric.GetField("status"); ok {
|
||||
status := g.DefaultServiceState
|
||||
switch s := s.(type) {
|
||||
case string:
|
||||
status = s
|
||||
case []byte:
|
||||
status = string(s)
|
||||
}
|
||||
if validStatus(status) {
|
||||
serviceObject.Status = transit.MonitorStatus(status)
|
||||
return
|
||||
}
|
||||
}
|
||||
status, err := transit.CalculateServiceStatus(&serviceObject.Metrics)
|
||||
if err != nil {
|
||||
g.Log.Infof("could not calculate service status, reverting to default_service_state: %v", err)
|
||||
status = transit.MonitorStatus(g.DefaultServiceState)
|
||||
}
|
||||
serviceObject.Status = status
|
||||
}()
|
||||
|
||||
return metricMeta{resource: resource, group: group}, &serviceObject
|
||||
}
|
||||
|
||||
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
|
||||
}
|
401
plugins/outputs/groundwork/groundwork_test.go
Normal file
401
plugins/outputs/groundwork/groundwork_test.go
Normal file
|
@ -0,0 +1,401 @@
|
|||
package groundwork
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gwos/tcg/sdk/clients"
|
||||
"github.com/gwos/tcg/sdk/transit"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/logger"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTestAgentID = "ec1676cc-583d-48ee-b035-7fb5ed0fcf88"
|
||||
defaultHost = "telegraf"
|
||||
defaultAppType = "TELEGRAF"
|
||||
customAppType = "SYSLOG"
|
||||
)
|
||||
|
||||
func TestWriteWithDebug(t *testing.T) {
|
||||
// Generate test metric with default name to test Write logic
|
||||
intMetric := testutil.TestMetric(42, "IntMetric")
|
||||
srvTok := "88fcf0de5bf7-530b-ee84-d385-cc6761ce"
|
||||
|
||||
// Simulate Groundwork server that should receive custom metrics
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode body to use in assertions below
|
||||
var obj transit.ResourcesWithServicesRequest
|
||||
if err = json.Unmarshal(body, &obj); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if server gets proper data
|
||||
if obj.Resources[0].Services[0].Name != "IntMetric" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "IntMetric", obj.Resources[0].Services[0].Name)
|
||||
return
|
||||
}
|
||||
if *obj.Resources[0].Services[0].Metrics[0].Value.IntegerValue != int64(42) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %v, actual: %v", int64(42), *obj.Resources[0].Services[0].Metrics[0].Value.IntegerValue)
|
||||
return
|
||||
}
|
||||
|
||||
// Send back details
|
||||
ans := "Content-type: application/json\n\n" + `{"message":"` + srvTok + `"}`
|
||||
if _, err = fmt.Fprintln(w, ans); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
i := Groundwork{
|
||||
Server: server.URL,
|
||||
AgentID: defaultTestAgentID,
|
||||
Username: config.NewSecret([]byte(`tu ser`)),
|
||||
Password: config.NewSecret([]byte(`pu ser`)),
|
||||
DefaultAppType: defaultAppType,
|
||||
DefaultHost: defaultHost,
|
||||
DefaultServiceState: string(transit.ServiceOk),
|
||||
ResourceTag: "host",
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
require.NoError(t, logger.SetupLogging(&logger.Config{Debug: true}))
|
||||
logger.RedirectLogging(buf)
|
||||
|
||||
require.NoError(t, i.Init())
|
||||
require.NoError(t, i.Write([]telegraf.Metric{intMetric}))
|
||||
|
||||
require.NoError(t, logger.CloseLogging())
|
||||
require.Contains(t, buf.String(), defaultTestAgentID)
|
||||
require.Contains(t, buf.String(), srvTok)
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
func TestWriteWithDefaults(t *testing.T) {
|
||||
// Generate test metric with default name to test Write logic
|
||||
intMetric := testutil.TestMetric(42, "IntMetric")
|
||||
|
||||
// Simulate Groundwork server that should receive custom metrics
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode body to use in assertions below
|
||||
var obj transit.ResourcesWithServicesRequest
|
||||
if err = json.Unmarshal(body, &obj); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if server gets proper data
|
||||
if obj.Context.AgentID != defaultTestAgentID {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", defaultTestAgentID, obj.Context.AgentID)
|
||||
return
|
||||
}
|
||||
if obj.Context.AppType != customAppType {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", customAppType, obj.Context.AppType)
|
||||
return
|
||||
}
|
||||
if obj.Resources[0].Name != defaultHost {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", defaultHost, obj.Resources[0].Name)
|
||||
return
|
||||
}
|
||||
if obj.Resources[0].Services[0].Status != transit.MonitorStatus("SERVICE_OK") {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", transit.MonitorStatus("SERVICE_OK"), obj.Resources[0].Services[0].Status)
|
||||
return
|
||||
}
|
||||
if obj.Resources[0].Services[0].Name != "IntMetric" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "IntMetric", obj.Resources[0].Services[0].Name)
|
||||
return
|
||||
}
|
||||
if *obj.Resources[0].Services[0].Metrics[0].Value.IntegerValue != int64(42) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %v, actual: %v", int64(42), *obj.Resources[0].Services[0].Metrics[0].Value.IntegerValue)
|
||||
return
|
||||
}
|
||||
if len(obj.Groups) != 0 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("'obj.Groups' should not be empty")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintln(w, "OK"); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
i := Groundwork{
|
||||
Log: testutil.Logger{},
|
||||
Server: server.URL,
|
||||
AgentID: defaultTestAgentID,
|
||||
DefaultHost: defaultHost,
|
||||
DefaultAppType: customAppType,
|
||||
client: clients.GWClient{
|
||||
AppName: "telegraf",
|
||||
AppType: customAppType,
|
||||
GWConnection: &clients.GWConnection{
|
||||
HostName: server.URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := i.Write([]telegraf.Metric{intMetric})
|
||||
require.NoError(t, err)
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
func TestWriteWithFields(t *testing.T) {
|
||||
// Generate test metric with fields to test Write logic
|
||||
floatMetric := testutil.TestMetric(1.0, "FloatMetric")
|
||||
floatMetric.AddField("value_cr", 3.0)
|
||||
floatMetric.AddField("value_wn", 2.0)
|
||||
floatMetric.AddField("message", "Test Message")
|
||||
floatMetric.AddField("status", "SERVICE_WARNING")
|
||||
|
||||
// Simulate Groundwork server that should receive custom metrics
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode body to use in assertions below
|
||||
var obj transit.ResourcesWithServicesRequest
|
||||
if err = json.Unmarshal(body, &obj); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if server gets proper data
|
||||
if obj.Resources[0].Services[0].LastPluginOutput != "Test Message" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "Test Message", obj.Resources[0].Services[0].LastPluginOutput)
|
||||
return
|
||||
}
|
||||
if obj.Resources[0].Services[0].Status != transit.MonitorStatus("SERVICE_WARNING") {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", transit.MonitorStatus("SERVICE_WARNING"), obj.Resources[0].Services[0].Status)
|
||||
return
|
||||
}
|
||||
if dt := float64(1.0) - *obj.Resources[0].Services[0].Metrics[0].Value.DoubleValue; !testutil.WithinDefaultDelta(dt) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Max difference between %v and %v allowed is %v, but difference was %v",
|
||||
float64(1.0), *obj.Resources[0].Services[0].Metrics[0].Value.DoubleValue, testutil.DefaultDelta, dt)
|
||||
return
|
||||
}
|
||||
if dt := float64(3.0) - *obj.Resources[0].Services[0].Metrics[0].Thresholds[0].Value.DoubleValue; !testutil.WithinDefaultDelta(dt) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Max difference between %v and %v allowed is %v, but difference was %v",
|
||||
float64(3.0), *obj.Resources[0].Services[0].Metrics[0].Thresholds[0].Value.DoubleValue, testutil.DefaultDelta, dt)
|
||||
return
|
||||
}
|
||||
if dt := float64(2.0) - *obj.Resources[0].Services[0].Metrics[0].Thresholds[1].Value.DoubleValue; !testutil.WithinDefaultDelta(dt) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Max difference between %v and %v allowed is %v, but difference was %v",
|
||||
float64(2.0), *obj.Resources[0].Services[0].Metrics[0].Thresholds[1].Value.DoubleValue, testutil.DefaultDelta, dt)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintln(w, "OK"); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
i := Groundwork{
|
||||
Log: testutil.Logger{},
|
||||
Server: server.URL,
|
||||
AgentID: defaultTestAgentID,
|
||||
DefaultHost: defaultHost,
|
||||
DefaultAppType: defaultAppType,
|
||||
GroupTag: "group",
|
||||
ResourceTag: "host",
|
||||
client: clients.GWClient{
|
||||
AppName: "telegraf",
|
||||
AppType: defaultAppType,
|
||||
GWConnection: &clients.GWConnection{
|
||||
HostName: server.URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := i.Write([]telegraf.Metric{floatMetric})
|
||||
require.NoError(t, err)
|
||||
|
||||
server.Close()
|
||||
}
|
||||
|
||||
func TestWriteWithTags(t *testing.T) {
|
||||
// Generate test metric with tags to test Write logic
|
||||
floatMetric := testutil.TestMetric(1.0, "FloatMetric")
|
||||
floatMetric.AddField("value_cr", 3.0)
|
||||
floatMetric.AddField("value_wn", 2.0)
|
||||
floatMetric.AddField("message", "Test Message")
|
||||
floatMetric.AddField("status", "SERVICE_WARNING")
|
||||
floatMetric.AddTag("value_cr", "9.0")
|
||||
floatMetric.AddTag("value_wn", "6.0")
|
||||
floatMetric.AddTag("message", "Test Tag")
|
||||
floatMetric.AddTag("status", "SERVICE_PENDING")
|
||||
floatMetric.AddTag("group-tag", "Group01")
|
||||
floatMetric.AddTag("resource-tag", "Host01")
|
||||
floatMetric.AddTag("service", "Service01")
|
||||
floatMetric.AddTag("facility", "FACILITY")
|
||||
floatMetric.AddTag("severity", "SEVERITY")
|
||||
|
||||
// Simulate Groundwork server that should receive custom metrics
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode body to use in assertions below
|
||||
var obj transit.ResourcesWithServicesRequest
|
||||
if err = json.Unmarshal(body, &obj); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if server gets proper data
|
||||
if obj.Context.AgentID != defaultTestAgentID {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", defaultTestAgentID, obj.Context.AgentID)
|
||||
return
|
||||
}
|
||||
if obj.Context.AppType != defaultAppType {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", defaultAppType, obj.Context.AppType)
|
||||
return
|
||||
}
|
||||
if obj.Resources[0].Name != "Host01" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "Host01", obj.Resources[0].Name)
|
||||
return
|
||||
}
|
||||
if obj.Resources[0].Services[0].Name != "Service01" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "Service01", obj.Resources[0].Services[0].Name)
|
||||
return
|
||||
}
|
||||
if *obj.Resources[0].Services[0].Properties["facility"].StringValue != "FACILITY" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "FACILITY", *obj.Resources[0].Services[0].Properties["facility"].StringValue)
|
||||
return
|
||||
}
|
||||
if *obj.Resources[0].Services[0].Properties["severity"].StringValue != "SEVERITY" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "SEVERITY", *obj.Resources[0].Services[0].Properties["severity"].StringValue)
|
||||
return
|
||||
}
|
||||
if obj.Groups[0].GroupName != "Group01" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "Group01", obj.Groups[0].GroupName)
|
||||
return
|
||||
}
|
||||
if obj.Groups[0].Resources[0].Name != "Host01" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "Host01", obj.Groups[0].Resources[0].Name)
|
||||
return
|
||||
}
|
||||
if obj.Resources[0].Services[0].LastPluginOutput != "Test Tag" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", "Test Tag", obj.Resources[0].Services[0].LastPluginOutput)
|
||||
return
|
||||
}
|
||||
if obj.Resources[0].Services[0].Status != transit.MonitorStatus("SERVICE_PENDING") {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Not equal, expected: %q, actual: %q", transit.MonitorStatus("SERVICE_PENDING"), obj.Resources[0].Services[0].Status)
|
||||
return
|
||||
}
|
||||
if dt := float64(1.0) - *obj.Resources[0].Services[0].Metrics[0].Value.DoubleValue; !testutil.WithinDefaultDelta(dt) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Max difference between %v and %v allowed is %v, but difference was %v",
|
||||
float64(1.0), *obj.Resources[0].Services[0].Metrics[0].Value.DoubleValue, testutil.DefaultDelta, dt)
|
||||
return
|
||||
}
|
||||
if dt := float64(9.0) - *obj.Resources[0].Services[0].Metrics[0].Thresholds[0].Value.DoubleValue; !testutil.WithinDefaultDelta(dt) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Max difference between %v and %v allowed is %v, but difference was %v",
|
||||
float64(9.0), *obj.Resources[0].Services[0].Metrics[0].Thresholds[0].Value.DoubleValue, testutil.DefaultDelta, dt)
|
||||
return
|
||||
}
|
||||
if dt := float64(6.0) - *obj.Resources[0].Services[0].Metrics[0].Thresholds[1].Value.DoubleValue; !testutil.WithinDefaultDelta(dt) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Errorf("Max difference between %v and %v allowed is %v, but difference was %v",
|
||||
float64(6.0), *obj.Resources[0].Services[0].Metrics[0].Thresholds[1].Value.DoubleValue, testutil.DefaultDelta, dt)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = fmt.Fprintln(w, "OK"); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}))
|
||||
|
||||
i := Groundwork{
|
||||
Log: testutil.Logger{},
|
||||
Server: server.URL,
|
||||
AgentID: defaultTestAgentID,
|
||||
DefaultHost: defaultHost,
|
||||
DefaultAppType: defaultAppType,
|
||||
GroupTag: "group-tag",
|
||||
ResourceTag: "resource-tag",
|
||||
client: clients.GWClient{
|
||||
AppName: "telegraf",
|
||||
AppType: defaultAppType,
|
||||
GWConnection: &clients.GWConnection{
|
||||
HostName: server.URL,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := i.Write([]telegraf.Metric{floatMetric})
|
||||
require.NoError(t, err)
|
||||
|
||||
server.Close()
|
||||
}
|
87
plugins/outputs/groundwork/log_adapter.go
Normal file
87
plugins/outputs/groundwork/log_adapter.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package groundwork
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog" //nolint:depguard // Required for wrapping internal logging facility
|
||||
"strings"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
)
|
||||
|
||||
// newLogger creates telegraf.Logger adapter for slog.Logger
|
||||
func newLogger(l telegraf.Logger) *slog.Logger {
|
||||
return slog.New(&tlgHandler{Log: l})
|
||||
}
|
||||
|
||||
// tlgHandler translates slog.Record into telegraf.Logger call
|
||||
// inspired by https://github.com/golang/example/blob/master/slog-handler-guide/README.md
|
||||
type tlgHandler struct {
|
||||
attrs []slog.Attr
|
||||
groups []string
|
||||
|
||||
Log telegraf.Logger
|
||||
}
|
||||
|
||||
// Enabled implements slog.Handler interface
|
||||
// It interprets errors as errors and everything else as debug.
|
||||
func (h *tlgHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
if level == slog.LevelError {
|
||||
return h.Log.Level() >= telegraf.Error
|
||||
}
|
||||
return h.Log.Level() >= telegraf.Debug
|
||||
}
|
||||
|
||||
// Handle implements slog.Handler interface
|
||||
// It interprets errors as errors and everything else as debug.
|
||||
func (h *tlgHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
attrs := make([]slog.Attr, 0, 2+len(h.attrs)+r.NumAttrs())
|
||||
attrs = append(attrs,
|
||||
slog.String("logger", strings.Join(h.groups, ",")),
|
||||
slog.String("message", r.Message),
|
||||
)
|
||||
attrs = append(attrs, h.attrs...)
|
||||
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
if v, ok := attr.Value.Any().(json.RawMessage); ok {
|
||||
attrs = append(attrs, slog.String(attr.Key, string(v)))
|
||||
return true
|
||||
}
|
||||
attrs = append(attrs, attr)
|
||||
return true
|
||||
})
|
||||
|
||||
if r.Level == slog.LevelError {
|
||||
h.Log.Error(attrs)
|
||||
} else {
|
||||
h.Log.Debug(attrs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithAttrs implements slog.Handler interface
|
||||
func (h *tlgHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
nested := &tlgHandler{Log: h.Log}
|
||||
nested.attrs = append(nested.attrs, h.attrs...)
|
||||
nested.groups = append(nested.groups, h.groups...)
|
||||
|
||||
for _, attr := range attrs {
|
||||
if v, ok := attr.Value.Any().(json.RawMessage); ok {
|
||||
nested.attrs = append(nested.attrs, slog.String(attr.Key, string(v)))
|
||||
continue
|
||||
}
|
||||
nested.attrs = append(nested.attrs, attr)
|
||||
}
|
||||
|
||||
return nested
|
||||
}
|
||||
|
||||
// WithGroup implements slog.Handler interface
|
||||
func (h *tlgHandler) WithGroup(name string) slog.Handler {
|
||||
nested := &tlgHandler{Log: h.Log}
|
||||
nested.attrs = append(nested.attrs, h.attrs...)
|
||||
nested.groups = append(nested.groups, h.groups...)
|
||||
nested.groups = append(nested.groups, name)
|
||||
return nested
|
||||
}
|
26
plugins/outputs/groundwork/sample.conf
Normal file
26
plugins/outputs/groundwork/sample.conf
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Send telegraf metrics to GroundWork Monitor
|
||||
[[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 application type to use in GroundWork client
|
||||
# default_app_type = "TELEGRAF"
|
||||
|
||||
## 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"
|
||||
|
||||
## The name of the tag that contains the host group name.
|
||||
# group_tag = "group"
|
Loading…
Add table
Add a link
Reference in a new issue