1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View 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

View 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
}

View 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()
}

View 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
}

View 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"