From a79c6fef37b750fee3e7b459a219a0d946113454 Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Tue, 13 Jul 2021 18:13:55 +0300 Subject: [PATCH 01/11] feat: add new gw8 output plugin --- plugins/outputs/all/all.go | 1 + plugins/outputs/gw8/README.md | 24 ++ plugins/outputs/gw8/addons/milliseconds.go | 106 ++++++ plugins/outputs/gw8/addons/request.go | 66 ++++ plugins/outputs/gw8/addons/types.go | 291 ++++++++++++++ plugins/outputs/gw8/gw8.go | 421 +++++++++++++++++++++ plugins/outputs/gw8/gw8_test.go | 61 +++ 7 files changed, 970 insertions(+) create mode 100644 plugins/outputs/gw8/README.md create mode 100644 plugins/outputs/gw8/addons/milliseconds.go create mode 100644 plugins/outputs/gw8/addons/request.go create mode 100644 plugins/outputs/gw8/addons/types.go create mode 100644 plugins/outputs/gw8/gw8.go create mode 100644 plugins/outputs/gw8/gw8_test.go diff --git a/plugins/outputs/all/all.go b/plugins/outputs/all/all.go index 7248b4ddcddb0..44beb20894fe3 100644 --- a/plugins/outputs/all/all.go +++ b/plugins/outputs/all/all.go @@ -20,6 +20,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/gw8" _ "github.com/influxdata/telegraf/plugins/outputs/health" _ "github.com/influxdata/telegraf/plugins/outputs/http" _ "github.com/influxdata/telegraf/plugins/outputs/influxdb" diff --git a/plugins/outputs/gw8/README.md b/plugins/outputs/gw8/README.md new file mode 100644 index 0000000000000..937821dedfd18 --- /dev/null +++ b/plugins/outputs/gw8/README.md @@ -0,0 +1,24 @@ +# Groundwork Output Plugin + +This plugin writes to a Groundwork instance using "[GROUNDWORK][]" own format. + +[GROUNDWORK]: https://github.com/gwos + +### Configuration: + +```toml +[[outputs.gw8]] + ## HTTP endpoint for your groundwork instance. + groundwork_endpoint = "" + + ## Agent uuid for Groundwork API Server + agent_id = "" + + ## Groundwork application type + app_type = "" + + ## Username to access Groundwork API + username = "" + ## Password to use in pair with username + password = "" +``` \ No newline at end of file diff --git a/plugins/outputs/gw8/addons/milliseconds.go b/plugins/outputs/gw8/addons/milliseconds.go new file mode 100644 index 0000000000000..f3d96fc64b565 --- /dev/null +++ b/plugins/outputs/gw8/addons/milliseconds.go @@ -0,0 +1,106 @@ +package addons + +import ( + "bytes" + "strconv" + "time" +) + +// MillisecondTimestamp refers to the JSON representation of timestamps, for +// time-data interchange, as a single integer representing a modified version of +// whole milliseconds since the UNIX epoch (00:00:00 UTC on January 1, 1970). +// Individual languages (Go, C, Java) will typically implement this structure +// using a more-complex construction in their respective contexts, containing even +// finer granularity for local data storage, typically at the nanosecond level. +// +// The "modified version" comment reflects the following simplification. +// Despite the already fine-grained representation as milliseconds, this data +// value takes no account of leap seconds; for all of our calculations, we +// simply pretend they don't exist. Individual feeders will typically map a +// 00:00:60 value for a leap second, obtained as a string so the presence of the +// leap second is obvious, as 00:01:00, and the fact that 00:01:00 will occur +// again in the following second will be silently ignored. This means that any +// monitoring which really wants to accurately reflect International Atomic Time +// (TAI), UT1, or similar time coordinates will be subject to some disruption. +// It also means that even in ordinary circumstances, any calculations of +// sub-second time differences might run into surprises, since the following +// timestamps could appear in temporal order: +// +// actual time relative reported time in milliseconds +// A: 00:00:59.000 59000 +// B: 00:00:60.000 60000 +// C: 00:00:60.700 60700 +// D: 00:01:00.000 60000 +// E: 00:01:00.300 60300 +// F: 00:01:01.000 61000 +// +// In such a situation, (D - C) and (E - C) would be negative numbers. +// +// In other situations, a feeder might obtain a timestamp from a system hardware +// clock which, say, counts local nanoseconds and has no notion of any leap +// seconds having been inserted into human-readable string-time representations. +// So there could be some amount of offset if such values are compared across +// such a boundary. +// +// Beyond that, there is always the issue of computer clocks not being directly +// tied to atomic clocks, using inexpensive non-temperature-compensated crystals +// for timekeeping. Such hardware can easily drift dramatically off course, and +// the local timekeeping may or may not be subject to course correction using +// HTP, chrony, or similar software that periodically adjusts the system time +// to keep it synchronized with the Internet. Also, there may be large jumps +// in either a positive or negative direction when a drifted clock is suddenly +// brought back into synchronization with the rest of the world. +// +// In addition, we ignore here all temporal effects of Special Relativity, not +// to mention further adjustments needed to account for General Relativity. +// This is not a theoretical joke; those who monitor GPS satellites should take +// note of the limitations of this data type, and use some other data type for +// time-critical data exchange and calculations. +// +// The point of all this being, fine resolution of clock values should never be +// taken too seriously unless one is sure that the clocks being compared are +// directly hitched together, and even then one must allow for quantum leaps +// into the future and time travel into the past. +// +// Finally, note that the Go zero-value of the internal implementation object +// we use in that language does not have a reasonable value when interpreted +// as milliseconds since the UNIX epoch. For that reason, the general rule is +// that the JSON representation of a zero-value for any field of this type, no +// matter what the originating language, will be to simply omit it from the +// JSON string. That fact must be taken into account when marshalling and +// unmarshalling data structures that contain such fields. +// +type MillisecondTimestamp struct { + time.Time +} + +// UnmarshalJSON implements json.Unmarshaler. +func (t *MillisecondTimestamp) UnmarshalJSON(input []byte) error { + strInput := string(bytes.Trim(input, "\"")) + + i, err := strconv.ParseInt(strInput, 10, 64) + if err != nil { + return err + } + + i *= int64(time.Millisecond) + *t = MillisecondTimestamp{time.Unix(0, i).UTC()} + return nil +} + +// MarshalJSON implements json.Marshaler. +func (t MillisecondTimestamp) MarshalJSON() ([]byte, error) { + i := t.UnixNano()/int64(time.Millisecond) + buf := make([]byte, 0, 16) + buf = append(buf, '"') + buf = strconv.AppendInt(buf, i, 10) + buf = append(buf, '"') + return buf, nil +} + +func (t MillisecondTimestamp) String() string { + i := t.UnixNano()/int64(time.Millisecond) + buf := make([]byte, 0, 16) + buf = strconv.AppendInt(buf, i, 10) + return string(buf) +} diff --git a/plugins/outputs/gw8/addons/request.go b/plugins/outputs/gw8/addons/request.go new file mode 100644 index 0000000000000..afe45b71dbe3b --- /dev/null +++ b/plugins/outputs/gw8/addons/request.go @@ -0,0 +1,66 @@ +package addons + +import ( + "bytes" + "crypto/tls" + "io" + "io/ioutil" + "net/http" + "net/url" +) + +var httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, +} + +// SendRequest wraps HTTP methods +func SendRequest(httpMethod string, requestURL string, + headers map[string]string, formValues map[string]string, byteBody []byte) (int, []byte, error) { + + var request *http.Request + var response *http.Response + var err error + + urlValues := url.Values{} + if formValues != nil { + for key, value := range formValues { + urlValues.Add(key, value) + } + byteBody = []byte(urlValues.Encode()) + } + + var body io.Reader + if byteBody != nil { + body = bytes.NewBuffer(byteBody) + } else { + body = nil + } + + request, err = http.NewRequest(httpMethod, requestURL, body) + + if err != nil { + return -1, nil, err + } + + request.Header.Set("Connection", "close") + if headers != nil { + for key, value := range headers { + request.Header.Add(key, value) + } + } + + response, err = httpClient.Do(request) + if err != nil { + return -1, nil, err + } + + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + return -1, nil, err + } + return response.StatusCode, responseBody, nil +} diff --git a/plugins/outputs/gw8/addons/types.go b/plugins/outputs/gw8/addons/types.go new file mode 100644 index 0000000000000..d981b131771e2 --- /dev/null +++ b/plugins/outputs/gw8/addons/types.go @@ -0,0 +1,291 @@ +package addons + +// MonitorStatusWeightService defines weight of Monitor Status for multi-state comparison +var MonitorStatusWeightService = map[MonitorStatus]int{ + ServiceOk: 0, + ServicePending: 10, + ServiceUnknown: 20, + ServiceWarning: 30, + ServiceScheduledCritical: 50, + ServiceUnscheduledCritical: 100, +} + +// MetricSampleType defines TimeSeries Metric Sample Possible Types +type MetricSampleType string + +// TimeSeries Metric Sample Possible Types +const ( + Value MetricSampleType = "Value" + Warning = "Warning" + Critical = "Critical" + Min = "Min" + Max = "Max" +) + +// UnitType - Supported units are a subset of The Unified Code for Units of Measure +// (http://unitsofmeasure.org/ucum.html) standard, added as we encounter +// the need for them in monitoring contexts. +type UnitType string + +// Supported units +const ( + UnitCounter UnitType = "1" + PercentCPU = "%{cpu}" + KB = "KB" + MB = "MB" + GB = "GB" +) + +// ComputeType defines CloudHub Compute Types +type ComputeType string + +// CloudHub Compute Types +const ( + Query ComputeType = "Query" + Regex = "Regex" + Synthetic = "Synthetic" + Informational = "Informational" + Performance = "Performance" + Health = "Health" +) + +// ValueType defines the data type of the value of a metric +type ValueType string + +// Data type of the value of a metric +const ( + IntegerType ValueType = "IntegerType" + DoubleType = "DoubleType" + StringType = "StringType" + BooleanType = "BooleanType" + TimeType = "TimeType" + UnspecifiedType = "UnspecifiedType" +) + +type MonitorStatus string + +const ( + ServiceOk MonitorStatus = "SERVICE_OK" + ServiceWarning MonitorStatus = "SERVICE_WARNING" + ServiceUnscheduledCritical MonitorStatus = "SERVICE_UNSCHEDULED_CRITICAL" + ServicePending MonitorStatus = "SERVICE_PENDING" + ServiceScheduledCritical MonitorStatus = "SERVICE_SCHEDULED_CRITICAL" + ServiceUnknown MonitorStatus = "SERVICE_UNKNOWN" + HostUp MonitorStatus = "HOST_UP" + HostUnscheduledDown MonitorStatus = "HOST_UNSCHEDULED_DOWN" + HostPending MonitorStatus = "HOST_PENDING" + HostScheduledDown MonitorStatus = "HOST_SCHEDULED_DOWN" + HostUnreachable MonitorStatus = "HOST_UNREACHABLE" + HostUnchanged MonitorStatus = "HOST_UNCHANGED" +) + +// ResourceType defines the resource type +type ResourceType string + +// The resource type uniquely defining the resource type +// General Nagios Types are host and service, whereas CloudHub can have richer complexity +const ( + Host ResourceType = "host" + Hypervisor = "hypervisor" + Instance = "instance" + VirtualMachine = "virtual-machine" + CloudApp = "cloud-app" + CloudFunction = "cloud-function" + LoadBalancer = "load-balancer" + Container = "container" + Storage = "storage" + Network = "network" + NetworkSwitch = "network-switch" + NetworkDevice = "network-device" +) + +// TypedValue defines a single strongly-typed value. +type TypedValue struct { + ValueType ValueType `json:"valueType"` + + // BoolValue: A Boolean value: true or false. + BoolValue bool `json:"boolValue,omitempty"` + + // DoubleValue: A 64-bit double-precision floating-point number. Its + // magnitude is approximately ±10±300 and it + // has 16 significant digits of precision. + DoubleValue float64 `json:"doubleValue"` + + // Int64Value: A 64-bit integer. Its range is approximately + // ±9.2x1018. + IntegerValue int64 `json:"integerValue"` + + // StringValue: A variable-length string value. + StringValue string `json:"stringValue,omitempty"` + + // a time stored as full timestamp + TimeValue *MillisecondTimestamp `json:"timeValue,omitempty"` +} + +// TimeInterval defines a closed time interval. It extends from the start time +// to the end time, and includes both: [startTime, endTime]. Valid time +// intervals depend on the MetricKind of the metric value. In no case +// can the end time be earlier than the start time. +// For a GAUGE metric, the StartTime value is technically optional; if +// no value is specified, the start time defaults to the value of the +// end time, and the interval represents a single point in time. Such an +// interval is valid only for GAUGE metrics, which are point-in-time +// measurements. +// For DELTA and CUMULATIVE metrics, the start time must be earlier +// than the end time. +// In all cases, the start time of the next interval must be at least a +// microsecond after the end time of the previous interval. Because the +// interval is closed, if the start time of a new interval is the same +// as the end time of the previous interval, data written at the new +// start time could overwrite data written at the previous end time. +type TimeInterval struct { + // EndTime: Required. The end of the time interval. + EndTime MillisecondTimestamp `json:"endTime,omitempty"` + + // StartTime: Optional. The beginning of the time interval. The default + // value for the start time is the end time. The start time must not be + // later than the end time. + StartTime MillisecondTimestamp `json:"startTime,omitempty"` +} + +type DynamicMonitoredResource struct { + // The unique name of the resource + Name string `json:"name,required"` + // Type: Required. The resource type of the resource + // General Nagios Types are hosts, whereas CloudHub can have richer complexity + Type ResourceType `json:"type,required"` + // Owner relationship for associations like hypervisor->virtual machine + Owner string `json:"owner,omitempty"` + // CloudHub Categorization of resources + Category string `json:"category,omitempty"` + // Optional description of this resource, such as Nagios notes + Description string `json:"description,omitempty"` + // Foundation Properties + Properties map[string]TypedValue `json:"properties,omitempty"` + // Device (usually IP address), leave empty if not available, will default to name + Device string `json:"device,omitempty"` + // Restrict to a Groundwork Monitor Status + Status MonitorStatus `json:"status,required"` + // The last status check time on this resource + LastCheckTime MillisecondTimestamp `json:"lastCheckTime,omitempty"` + // The next status check time on this resource + NextCheckTime MillisecondTimestamp `json:"nextCheckTime,omitempty"` + // Nagios plugin output string + LastPlugInOutput string `json:"lastPluginOutput,omitempty"` + // Services state collection + Services []DynamicMonitoredService `json:"services"` +} + +// A DynamicMonitoredService represents a Groundwork Service creating during a metrics scan. +// In cloud systems, services are usually modeled as a complex metric definition, with each sampled +// metric variation represented as as single metric time series. +// +// A DynamicMonitoredService contains a collection of TimeSeries Metrics. +// MonitoredService collections are attached to a DynamicMonitoredResource during a metrics scan. +type DynamicMonitoredService struct { + // The unique name of the resource + Name string `json:"name,required"` + // Type: Required. The resource type of the resource + // General Nagios Types are hosts, whereas CloudHub can have richer complexity + Type ResourceType `json:"type,required"` + // Owner relationship for associations like hypervisor->virtual machine + Owner string `json:"owner,omitempty"` + // CloudHub Categorization of resources + Category string `json:"category,omitempty"` + // Optional description of this resource, such as Nagios notes + Description string `json:"description,omitempty"` + // Foundation Properties + Properties map[string]TypedValue `json:"properties,omitempty"` + // Restrict to a Groundwork Monitor Status + Status MonitorStatus `json:"status,required"` + // The last status check time on this resource + LastCheckTime MillisecondTimestamp `json:"lastCheckTime,omitempty"` + // The next status check time on this resource + NextCheckTime MillisecondTimestamp `json:"nextCheckTime,omitempty"` + // Nagios plugin output string + LastPlugInOutput string `json:"lastPluginOutput,omitempty"` + // metrics + Metrics []TimeSeries `json:"metrics"` +} + +// ThresholdValue describes threshold +type ThresholdValue struct { + SampleType MetricSampleType `json:"sampleType"` + Label string `json:"label"` + Value *TypedValue `json:"value"` +} + +// TimeSeries defines a single Metric Sample, its time interval, and 0 or more thresholds +type TimeSeries struct { + MetricName string `json:"metricName"` + SampleType MetricSampleType `json:"sampleType,omitEmpty"` + // Interval: The time interval to which the data sample applies. For + // GAUGE metrics, only the end time of the interval is used. For DELTA + // metrics, the start and end time should specify a non-zero interval, + // with subsequent samples specifying contiguous and non-overlapping + // intervals. For CUMULATIVE metrics, the start and end time should + // specify a non-zero interval, with subsequent samples specifying the + // same start time and increasing end times, until an event resets the + // cumulative value to zero and sets a new start time for the following + // samples. + Interval *TimeInterval `json:"interval"` + Value *TypedValue `json:"value"` + Tags map[string]string `json:"tags,omitempty"` + Unit UnitType `json:"unit,omitempty"` + Thresholds *[]ThresholdValue `json:"thresholds,omitempty"` + MetricComputeType ComputeType `json:"-"` + MetricExpression string `json:"-"` +} + +// DynamicResourcesWithServicesRequest defines SendResourcesWithMetrics payload +type DynamicResourcesWithServicesRequest struct { + Context *TracerContext `json:"context,omitempty"` + Resources []DynamicMonitoredResource `json:"resources"` + Groups []ResourceGroup `json:"groups,omitempty"` +} + +// ResourceGroup defines group entity +type ResourceGroup struct { + GroupName string `json:"groupName,required"` + Type GroupType `json:"type,required"` + Description string `json:"description,omitempty"` + Resources []MonitoredResourceRef `json:"resources,required"` +} + +// GroupType defines the foundation group type +type GroupType string + +// The group type uniquely defining corresponding foundation group type +const ( + HostGroup GroupType = "HostGroup" + ServiceGroup = "ServiceGroup" + CustomGroup = "CustomGroup" +) + +// MonitoredResourceRef references a MonitoredResource in a group collection +type MonitoredResourceRef struct { + // The unique name of the resource + Name string `json:"name,required"` + // Type: Optional. The resource type uniquely defining the resource type + // General Nagios Types are host and service, whereas CloudHub can have richer complexity + Type ResourceType `json:"type,omitempty"` + // Owner relationship for associations like host->service + Owner string `json:"owner,omitempty"` +} + +// TracerContext describes a Transit call +type TracerContext struct { + AppType string `json:"appType"` + AgentID string `json:"agentId"` + TraceToken string `json:"traceToken"` + TimeStamp MillisecondTimestamp `json:"timeStamp"` + Version VersionString `json:"version"` +} + +// VersionString defines type of constant +type VersionString string + +// ModelVersion defines versioning +const ( + ModelVersion VersionString = "1.0.0" +) diff --git a/plugins/outputs/gw8/gw8.go b/plugins/outputs/gw8/gw8.go new file mode 100644 index 0000000000000..b4c41c68a3bbc --- /dev/null +++ b/plugins/outputs/gw8/gw8.go @@ -0,0 +1,421 @@ +package gw8 + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/outputs" + "github.com/influxdata/telegraf/plugins/outputs/gw8/addons" + "io" + "net/http" + "strconv" + "time" +) + +const ( + defaultMonitoringRoute = "/api/monitoring?dynamic=true" +) + +const ( + loginUrl = "/api/auth/login" + logoutUrl = "/api/auth/logout" +) + +var sampleConfig = ` + ## HTTP endpoint for your groundwork instance. + groundwork_endpoint = "" + + ## Agent uuid for Groundwork API Server + agent_id = "" + + ## Groundwork application type + app_type = "" + + ## Username to access Groundwork API + username = "" + ## Password to use in pair with username + password = "" + + ## Default display name for the host with services(metrics) + default_host = "default_telegraf" +` + +type GW8 struct { + Server string `toml:"groundwork_endpoint"` + AgentId string `toml:"agent_id"` + AppType string `toml:"app_type"` + Username string `toml:"username"` + Password string `toml:"password"` + DefaultHost string `toml:"default_host"` + authToken string + writer io.Writer +} + +func (g *GW8) SampleConfig() string { + return sampleConfig +} + +func (g *GW8) Connect() error { + var writers []io.Writer + + if g.Server == "" { + return errors.New("Groundwork endpoint\\username\\password are not provided ") + } + + if byteToken, err := login(g.Server+loginUrl, g.Username, g.Password); err == nil { + g.authToken = string(byteToken) + } else { + return err + } + + g.writer = io.MultiWriter(writers...) + + return nil +} + +func (g *GW8) Close() error { + formValues := map[string]string{ + "gwos-app-name": "gw8", + "gwos-api-token": g.authToken, + } + + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + + _, _, err := addons.SendRequest(http.MethodPost, g.Server+logoutUrl, headers, formValues, nil) + if err != nil { + return err + } + + return nil +} + +func (g *GW8) Write(metrics []telegraf.Metric) error { + resourceToServicesMap := make(map[string][]addons.DynamicMonitoredService) + for _, metric := range metrics { + resource, service := parseMetric(g.DefaultHost, metric) + resourceToServicesMap[resource] = append(resourceToServicesMap[resource], service) + } + + var resources []addons.DynamicMonitoredResource + for resourceName, services := range resourceToServicesMap { + resources = append(resources, addons.DynamicMonitoredResource{ + Name: resourceName, + Type: addons.Host, + Status: addons.HostUp, + LastCheckTime: addons.MillisecondTimestamp{Time: time.Now()}, + NextCheckTime: addons.MillisecondTimestamp{Time: time.Now()}, + Services: services, + }) + } + + requestJson, err := json.Marshal(addons.DynamicResourcesWithServicesRequest{ + Context: &addons.TracerContext{ + AppType: g.AppType, + AgentID: g.AgentId, + TraceToken: "cd05607a-4d23-4338-95db-c96367034d23", + TimeStamp: addons.MillisecondTimestamp{Time: time.Now()}, + Version: addons.ModelVersion, + }, + Resources: resources, + Groups: nil, + }) + + if err != nil { + return err + } + + headers := map[string]string{ + "GWOS-APP-NAME": "gw8", + "GWOS-API-TOKEN": g.authToken, + "Content-Type": "application/json", + "Accept": "application/json", + } + + if _, _, err = addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJson); err != nil { + return err + } + + return nil +} + +func parseMetric(hostname string, metric telegraf.Metric) (string, addons.DynamicMonitoredService) { + resource := "default_telegraf" + if hostname != "" { + resource = hostname + } + if value, present := metric.GetTag("resource"); present { + resource = value + } + + service := metric.Name() + if value, present := metric.GetTag("service"); present { + service = value + } + + status := string(addons.ServiceOk) + if value, present := metric.GetTag("status"); present { + if validStatus(value) { + status = value + } + } + + message := "" + if value, present := metric.GetTag("message"); present { + message = value + } + + unitType := string(addons.UnitCounter) + if value, present := metric.GetTag("unitType"); present { + unitType = value + } + + critical := -1.0 + if value, present := metric.GetTag("critical"); present { + if s, err := strconv.ParseFloat(value, 64); err == nil { + critical = s + } + unitType = value + } + + warning := -1.0 + if value, present := metric.GetTag("warning"); present { + if s, err := strconv.ParseFloat(value, 64); err == nil { + warning = s + } + unitType = value + } + + serviceObject := addons.DynamicMonitoredService{ + Name: service, + Type: "Service", + Owner: resource, + Status: addons.MonitorStatus(status), + LastCheckTime: addons.MillisecondTimestamp{Time: time.Now()}, + NextCheckTime: addons.MillisecondTimestamp{Time: time.Now()}, + LastPlugInOutput: message, + Metrics: nil, + } + + for _, value := range metric.FieldList() { + var thresholds []addons.ThresholdValue + thresholds = append(thresholds, addons.ThresholdValue{ + SampleType: addons.Warning, + Label: value.Key + "_wn", + Value: &addons.TypedValue{ + ValueType: addons.DoubleType, + DoubleValue: warning, + }, + }) + thresholds = append(thresholds, addons.ThresholdValue{ + SampleType: addons.Critical, + Label: value.Key + "_cr", + Value: &addons.TypedValue{ + ValueType: addons.DoubleType, + DoubleValue: critical, + }, + }) + + var val float64 + switch i := value.Value.(type) { + case float64: + val = i + case float32: + val = float64(i) + case int64: + val = float64(i) + case int: + val = float64(i) + default: + } + serviceObject.Metrics = append(serviceObject.Metrics, addons.TimeSeries{ + MetricName: value.Key, + SampleType: addons.Value, + Interval: &addons.TimeInterval{ + EndTime: addons.MillisecondTimestamp{Time: time.Now()}, + StartTime: addons.MillisecondTimestamp{Time: time.Now()}, + }, + Value: &addons.TypedValue{ + ValueType: addons.DoubleType, + DoubleValue: val, + }, + Unit: addons.UnitType(unitType), + Thresholds: &thresholds, + }) + } + + serviceObject.Status, _ = calculateServiceStatus(&serviceObject.Metrics) + + return resource, serviceObject +} + +func (g *GW8) Description() string { + return "Send telegraf metrics to groundwork" +} + +func login(url, username, password string) ([]byte, error) { + formValues := map[string]string{ + "user": username, + "password": password, + "gwos-app-name": "gw8", + } + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "text/plain", + } + + statusCode, body, err := addons.SendRequest(http.MethodPost, url, headers, formValues, nil) + if err != nil { + return nil, err + } + if statusCode != 200 { + return nil, errors.New(fmt.Sprintf("[ERROR]: Http request failed. [Status code]: %d, [Response]: %s", + statusCode, string(body))) + } + + return body, nil +} + +func calculateServiceStatus(metrics *[]addons.TimeSeries) (addons.MonitorStatus, error) { + if metrics == nil || len(*metrics) == 0 { + return addons.ServiceUnknown, nil + } + previousStatus := addons.ServiceOk + for _, metric := range *metrics { + if metric.Thresholds != nil { + var warning, critical addons.ThresholdValue + for _, threshold := range *metric.Thresholds { + switch threshold.SampleType { + case addons.Warning: + warning = threshold + case addons.Critical: + critical = threshold + default: + return addons.ServiceOk, fmt.Errorf("unsupported threshold Sample type") + } + } + + status := calculateStatus(metric.Value, warning.Value, critical.Value) + if addons.MonitorStatusWeightService[status] > addons.MonitorStatusWeightService[previousStatus] { + previousStatus = status + } + } + } + return previousStatus, nil +} + +func calculateStatus(value *addons.TypedValue, warning *addons.TypedValue, critical *addons.TypedValue) addons.MonitorStatus { + if warning == nil && critical == nil { + return addons.ServiceOk + } + + var warningValue float64 + var criticalValue float64 + + if warning != nil { + switch warning.ValueType { + case addons.IntegerType: + warningValue = float64(warning.IntegerValue) + case addons.DoubleType: + warningValue = warning.DoubleValue + } + } + + if critical != nil { + switch critical.ValueType { + case addons.IntegerType: + criticalValue = float64(critical.IntegerValue) + case addons.DoubleType: + criticalValue = critical.DoubleValue + } + } + + switch value.ValueType { + case addons.IntegerType: + if warning == nil && criticalValue == -1 { + if float64(value.IntegerValue) >= criticalValue { + return addons.ServiceUnscheduledCritical + } + return addons.ServiceOk + } + if critical == nil && (warning != nil && warningValue == -1) { + if float64(value.IntegerValue) >= warningValue { + return addons.ServiceWarning + } + return addons.ServiceOk + } + if (warning != nil && warningValue == -1) && (critical != nil && criticalValue == -1) { + return addons.ServiceOk + } + // is it a reverse comparison (low to high) + if (warning != nil && critical != nil) && warningValue > criticalValue { + if float64(value.IntegerValue) <= criticalValue { + return addons.ServiceUnscheduledCritical + } + if float64(value.IntegerValue) <= warningValue { + return addons.ServiceWarning + } + return addons.ServiceOk + } else { + if (warning != nil && critical != nil) && float64(value.IntegerValue) >= criticalValue { + return addons.ServiceUnscheduledCritical + } + if (warning != nil && critical != nil) && float64(value.IntegerValue) >= warningValue { + return addons.ServiceWarning + } + return addons.ServiceOk + } + case addons.DoubleType: + if warning == nil && criticalValue == -1 { + if value.DoubleValue >= criticalValue { + return addons.ServiceUnscheduledCritical + } + return addons.ServiceOk + } + if critical == nil && (warning != nil && warningValue == -1) { + if value.DoubleValue >= warningValue { + return addons.ServiceWarning + } + return addons.ServiceOk + } + if (warning != nil && critical != nil) && (warningValue == -1 || criticalValue == -1) { + return addons.ServiceOk + } + // is it a reverse comparison (low to high) + if warningValue > criticalValue { + if value.DoubleValue <= criticalValue { + return addons.ServiceUnscheduledCritical + } + if value.DoubleValue <= warningValue { + return addons.ServiceWarning + } + return addons.ServiceOk + } else { + if value.DoubleValue >= criticalValue { + return addons.ServiceUnscheduledCritical + } + if value.DoubleValue >= warningValue { + return addons.ServiceWarning + } + return addons.ServiceOk + } + } + return addons.ServiceOk +} + +func validStatus(status string) bool { + return status == string(addons.ServiceOk) || + status == string(addons.ServiceWarning) || + status == string(addons.ServicePending) || + status == string(addons.ServiceScheduledCritical) || + status == string(addons.ServiceUnscheduledCritical) || + status == string(addons.ServiceUnknown) +} + +func init() { + outputs.Add("gw8", func() telegraf.Output { + return &GW8{} + }) +} diff --git a/plugins/outputs/gw8/gw8_test.go b/plugins/outputs/gw8/gw8_test.go new file mode 100644 index 0000000000000..051bea581a5da --- /dev/null +++ b/plugins/outputs/gw8/gw8_test.go @@ -0,0 +1,61 @@ +package gw8 + +import ( + "encoding/json" + "fmt" + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/testutil" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +const ( + defaultTestAgentId = "ec1676cc-583d-48ee-b035-7fb5ed0fcf88" + defaultTestAppType = "TELEGRAF" + defaultTestServiceName = "GROUNDWORK_TEST" +) + +func TestWrite(t *testing.T) { + // Generate test metric with default name to test Write logic + metric := testutil.TestMetric(1, defaultTestServiceName) + + // 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) + assert.NoError(t, err) + + // Decode body to use in assertations below + var obj GroundworkObject + err = json.Unmarshal(body, &obj) + assert.NoError(t, err) + + // Check if server gets valid metrics object + assert.Equal(t, obj["context"].(map[string]interface{})["appType"], defaultTestAppType) + assert.Equal(t, obj["context"].(map[string]interface{})["agentId"], defaultTestAgentId) + assert.Equal(t, obj["resources"].([]interface{})[0].(map[string]interface{})["name"], "default_telegraf") + assert.Equal( + t, + obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[0].(map[string]interface{})["name"], + defaultTestServiceName, + ) + + _, err = fmt.Fprintln(w, `OK`) + assert.NoError(t, err) + })) + + i := GW8{ + Server: server.URL, + AppType: defaultTestAppType, + AgentId: defaultTestAgentId, + } + + err := i.Write([]telegraf.Metric{metric}) + assert.NoError(t, err) + + defer server.Close() +} + +type GroundworkObject map[string]interface{} From ce057f65d0b4960441ab246b8bf3031c2e26c840 Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Mon, 11 Oct 2021 17:54:26 +0300 Subject: [PATCH 02/11] feat: add re-login mechanism, logic to build unique trace token + minor fixes --- go.mod | 1 + go.sum | 2 + plugins/outputs/gw8/README.md | 2 +- plugins/outputs/gw8/addons/milliseconds.go | 85 ++++++++-- plugins/outputs/gw8/addons/types.go | 80 +++------ plugins/outputs/gw8/gw8.go | 186 ++++++++++++--------- plugins/outputs/gw8/gw8_test.go | 13 ++ 7 files changed, 213 insertions(+), 156 deletions(-) diff --git a/go.mod b/go.mod index c6f3138489d28..27f26486fea33 100644 --- a/go.mod +++ b/go.mod @@ -341,6 +341,7 @@ require ( go.opentelemetry.io/otel/sdk/export/metric v0.23.0 // indirect go.opentelemetry.io/otel/trace v1.0.0-RC3 // indirect go.opentelemetry.io/proto/otlp v0.9.0 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible ) // replaced due to https://github.com/satori/go.uuid/issues/73 diff --git a/go.sum b/go.sum index a8fc62a7b3874..47a8e90858146 100644 --- a/go.sum +++ b/go.sum @@ -1370,6 +1370,8 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= github.com/pavius/impi v0.0.0-20180302134524-c1cbdcb8df2b/go.mod h1:x/hU0bfdWIhuOT1SKwiJg++yvkk6EuOtJk8WtDZqgr8= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= diff --git a/plugins/outputs/gw8/README.md b/plugins/outputs/gw8/README.md index 937821dedfd18..aa98731e26d02 100644 --- a/plugins/outputs/gw8/README.md +++ b/plugins/outputs/gw8/README.md @@ -2,7 +2,7 @@ This plugin writes to a Groundwork instance using "[GROUNDWORK][]" own format. -[GROUNDWORK]: https://github.com/gwos +[GROUNDWORK]: https://www.gwos.com ### Configuration: diff --git a/plugins/outputs/gw8/addons/milliseconds.go b/plugins/outputs/gw8/addons/milliseconds.go index f3d96fc64b565..dcf162f03a414 100644 --- a/plugins/outputs/gw8/addons/milliseconds.go +++ b/plugins/outputs/gw8/addons/milliseconds.go @@ -6,7 +6,9 @@ import ( "time" ) -// MillisecondTimestamp refers to the JSON representation of timestamps, for +// Timestamp wraps time.Time and adapts json.Marshaler. +// +// Timestamp refers to the JSON representation of timestamps, for // time-data interchange, as a single integer representing a modified version of // whole milliseconds since the UNIX epoch (00:00:00 UTC on January 1, 1970). // Individual languages (Go, C, Java) will typically implement this structure @@ -70,27 +72,61 @@ import ( // JSON string. That fact must be taken into account when marshalling and // unmarshalling data structures that contain such fields. // -type MillisecondTimestamp struct { +type Timestamp struct { time.Time } -// UnmarshalJSON implements json.Unmarshaler. -func (t *MillisecondTimestamp) UnmarshalJSON(input []byte) error { - strInput := string(bytes.Trim(input, "\"")) +// NewTimestamp returns reference to new timestamp setted to UTC now. +func NewTimestamp() *Timestamp { + return &Timestamp{time.Now().UTC()} +} - i, err := strconv.ParseInt(strInput, 10, 64) - if err != nil { - return err - } +// Add returns the timestamp t+d. +// Overrides nested time.Time Add. +func (t Timestamp) Add(d time.Duration) Timestamp { + return Timestamp{t.Time.Add(d)} +} - i *= int64(time.Millisecond) - *t = MillisecondTimestamp{time.Unix(0, i).UTC()} - return nil +// AddDate returns the timestamp corresponding to adding the given number of years, months, and days. +// Overrides nested time.Time AddDate. +func (t Timestamp) AddDate(years int, months int, days int) Timestamp { + return Timestamp{t.Time.AddDate(years, months, days)} +} + +// In returns a copy of t with location set to loc. +// Overrides nested time.Time In. +func (t Timestamp) In(loc *time.Location) Timestamp { + return Timestamp{t.Time.In(loc)} +} + +// Local returns a copy of t with the location set to local time. +// Overrides nested time.Time Local. +func (t Timestamp) Local() Timestamp { + return Timestamp{t.Time.Local()} +} + +// Round returns a copy of t rounded to the nearest multiple of d. +// Overrides nested time.Time Round. +func (t Timestamp) Round(d time.Duration) Timestamp { + return Timestamp{t.Time.Round(d)} +} + +// Truncate returns a copy t rounded down to a multiple of d. +// Overrides nested time.Time Truncate. +func (t Timestamp) Truncate(d time.Duration) Timestamp { + return Timestamp{t.Time.Truncate(d)} +} + +// UTC returns a copy of t with the location set to UTC. +// Overrides nested time.Time UTC. +func (t Timestamp) UTC() Timestamp { + return Timestamp{t.Time.UTC()} } // MarshalJSON implements json.Marshaler. -func (t MillisecondTimestamp) MarshalJSON() ([]byte, error) { - i := t.UnixNano()/int64(time.Millisecond) +// Overrides nested time.Time MarshalJSON. +func (t Timestamp) MarshalJSON() ([]byte, error) { + i := t.UnixMilli() buf := make([]byte, 0, 16) buf = append(buf, '"') buf = strconv.AppendInt(buf, i, 10) @@ -98,9 +134,26 @@ func (t MillisecondTimestamp) MarshalJSON() ([]byte, error) { return buf, nil } -func (t MillisecondTimestamp) String() string { - i := t.UnixNano()/int64(time.Millisecond) +// String implements fmt.Stringer. +// Overrides nested time.Time String. +func (t Timestamp) String() string { + i := t.UnixMilli() buf := make([]byte, 0, 16) buf = strconv.AppendInt(buf, i, 10) return string(buf) } + +// UnmarshalJSON implements json.Unmarshaler. +// Overrides nested time.Time UnmarshalJSON. +func (t *Timestamp) UnmarshalJSON(input []byte) error { + strInput := string(bytes.Trim(input, `"`)) + + i, err := strconv.ParseInt(strInput, 10, 64) + if err != nil { + return err + } + + i *= int64(time.Millisecond) + *t = Timestamp{time.Unix(0, i).UTC()} + return nil +} diff --git a/plugins/outputs/gw8/addons/types.go b/plugins/outputs/gw8/addons/types.go index d981b131771e2..bbe9f4a00a8c9 100644 --- a/plugins/outputs/gw8/addons/types.go +++ b/plugins/outputs/gw8/addons/types.go @@ -18,8 +18,6 @@ const ( Value MetricSampleType = "Value" Warning = "Warning" Critical = "Critical" - Min = "Min" - Max = "Max" ) // UnitType - Supported units are a subset of The Unified Code for Units of Measure @@ -36,30 +34,12 @@ const ( GB = "GB" ) -// ComputeType defines CloudHub Compute Types -type ComputeType string - -// CloudHub Compute Types -const ( - Query ComputeType = "Query" - Regex = "Regex" - Synthetic = "Synthetic" - Informational = "Informational" - Performance = "Performance" - Health = "Health" -) - // ValueType defines the data type of the value of a metric type ValueType string -// Data type of the value of a metric +// DoubleType type of the value of a metric const ( - IntegerType ValueType = "IntegerType" - DoubleType = "DoubleType" - StringType = "StringType" - BooleanType = "BooleanType" - TimeType = "TimeType" - UnspecifiedType = "UnspecifiedType" + DoubleType ValueType = "DoubleType" ) type MonitorStatus string @@ -82,21 +62,11 @@ const ( // ResourceType defines the resource type type ResourceType string -// The resource type uniquely defining the resource type +// Host The resource type uniquely defining the resource type // General Nagios Types are host and service, whereas CloudHub can have richer complexity const ( - Host ResourceType = "host" - Hypervisor = "hypervisor" - Instance = "instance" - VirtualMachine = "virtual-machine" - CloudApp = "cloud-app" - CloudFunction = "cloud-function" - LoadBalancer = "load-balancer" - Container = "container" - Storage = "storage" - Network = "network" - NetworkSwitch = "network-switch" - NetworkDevice = "network-device" + Host ResourceType = "host" + Service = "service" ) // TypedValue defines a single strongly-typed value. @@ -109,17 +79,17 @@ type TypedValue struct { // DoubleValue: A 64-bit double-precision floating-point number. Its // magnitude is approximately ±10±300 and it // has 16 significant digits of precision. - DoubleValue float64 `json:"doubleValue"` + DoubleValue float64 `json:"doubleValue,omitempty"` // Int64Value: A 64-bit integer. Its range is approximately // ±9.2x1018. - IntegerValue int64 `json:"integerValue"` + IntegerValue int64 `json:"integerValue,omitempty"` // StringValue: A variable-length string value. StringValue string `json:"stringValue,omitempty"` // a time stored as full timestamp - TimeValue *MillisecondTimestamp `json:"timeValue,omitempty"` + TimeValue *Timestamp `json:"timeValue,omitempty"` } // TimeInterval defines a closed time interval. It extends from the start time @@ -140,12 +110,12 @@ type TypedValue struct { // start time could overwrite data written at the previous end time. type TimeInterval struct { // EndTime: Required. The end of the time interval. - EndTime MillisecondTimestamp `json:"endTime,omitempty"` + EndTime *Timestamp `json:"endTime"` // StartTime: Optional. The beginning of the time interval. The default // value for the start time is the end time. The start time must not be // later than the end time. - StartTime MillisecondTimestamp `json:"startTime,omitempty"` + StartTime *Timestamp `json:"startTime,omitempty"` } type DynamicMonitoredResource struct { @@ -167,9 +137,9 @@ type DynamicMonitoredResource struct { // Restrict to a Groundwork Monitor Status Status MonitorStatus `json:"status,required"` // The last status check time on this resource - LastCheckTime MillisecondTimestamp `json:"lastCheckTime,omitempty"` + LastCheckTime *Timestamp `json:"lastCheckTime,omitempty"` // The next status check time on this resource - NextCheckTime MillisecondTimestamp `json:"nextCheckTime,omitempty"` + NextCheckTime *Timestamp `json:"nextCheckTime,omitempty"` // Nagios plugin output string LastPlugInOutput string `json:"lastPluginOutput,omitempty"` // Services state collection @@ -199,9 +169,7 @@ type DynamicMonitoredService struct { // Restrict to a Groundwork Monitor Status Status MonitorStatus `json:"status,required"` // The last status check time on this resource - LastCheckTime MillisecondTimestamp `json:"lastCheckTime,omitempty"` - // The next status check time on this resource - NextCheckTime MillisecondTimestamp `json:"nextCheckTime,omitempty"` + LastCheckTime *Timestamp `json:"lastCheckTime,omitempty"` // Nagios plugin output string LastPlugInOutput string `json:"lastPluginOutput,omitempty"` // metrics @@ -228,13 +196,11 @@ type TimeSeries struct { // same start time and increasing end times, until an event resets the // cumulative value to zero and sets a new start time for the following // samples. - Interval *TimeInterval `json:"interval"` - Value *TypedValue `json:"value"` - Tags map[string]string `json:"tags,omitempty"` - Unit UnitType `json:"unit,omitempty"` - Thresholds *[]ThresholdValue `json:"thresholds,omitempty"` - MetricComputeType ComputeType `json:"-"` - MetricExpression string `json:"-"` + Interval *TimeInterval `json:"interval"` + Value *TypedValue `json:"value"` + Tags map[string]string `json:"tags,omitempty"` + Unit UnitType `json:"unit,omitempty"` + Thresholds *[]ThresholdValue `json:"thresholds,omitempty"` } // DynamicResourcesWithServicesRequest defines SendResourcesWithMetrics payload @@ -275,11 +241,11 @@ type MonitoredResourceRef struct { // TracerContext describes a Transit call type TracerContext struct { - AppType string `json:"appType"` - AgentID string `json:"agentId"` - TraceToken string `json:"traceToken"` - TimeStamp MillisecondTimestamp `json:"timeStamp"` - Version VersionString `json:"version"` + AppType string `json:"appType"` + AgentID string `json:"agentId"` + TraceToken string `json:"traceToken"` + TimeStamp *Timestamp `json:"timeStamp"` + Version VersionString `json:"version"` } // VersionString defines type of constant diff --git a/plugins/outputs/gw8/gw8.go b/plugins/outputs/gw8/gw8.go index b4c41c68a3bbc..eafb43cb8b18a 100644 --- a/plugins/outputs/gw8/gw8.go +++ b/plugins/outputs/gw8/gw8.go @@ -1,15 +1,19 @@ package gw8 import ( + "encoding/binary" "encoding/json" "errors" "fmt" + "github.com/hashicorp/go-uuid" "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/plugins/outputs" "github.com/influxdata/telegraf/plugins/outputs/gw8/addons" - "io" + "github.com/patrickmn/go-cache" "net/http" "strconv" + "sync" "time" ) @@ -17,12 +21,25 @@ const ( defaultMonitoringRoute = "/api/monitoring?dynamic=true" ) +// Login and logout routes from Groundwork API const ( loginUrl = "/api/auth/login" logoutUrl = "/api/auth/logout" ) -var sampleConfig = ` +var ( + tracerOnce sync.Once +) + +// Variables for building and updating tracer token +var ( + tracerToken []byte + cacheKeyTracerToken = "cacheKeyTraceToken" + tracerCache = cache.New(-1, -1) +) + +var ( + sampleConfig = ` ## HTTP endpoint for your groundwork instance. groundwork_endpoint = "" @@ -39,17 +56,25 @@ var sampleConfig = ` ## Default display name for the host with services(metrics) default_host = "default_telegraf" + + ## Default service state [default - "host"] + default_service_state = "SERVICE_OK" + + ## The name of the tag that contains the hostname + resource_tag = "host" ` +) type GW8 struct { - Server string `toml:"groundwork_endpoint"` - AgentId string `toml:"agent_id"` - AppType string `toml:"app_type"` - Username string `toml:"username"` - Password string `toml:"password"` - DefaultHost string `toml:"default_host"` - authToken string - writer io.Writer + Server string `toml:"groundwork_endpoint"` + AgentId string `toml:"agent_id"` + AppType string `toml:"app_type"` + Username string `toml:"username"` + Password string `toml:"password"` + DefaultHost string `toml:"default_host"` + DefaultServiceState string `toml:"default_service_state"` + ResourceTag string `toml:"resource_tag"` + authToken string } func (g *GW8) SampleConfig() string { @@ -57,8 +82,6 @@ func (g *GW8) SampleConfig() string { } func (g *GW8) Connect() error { - var writers []io.Writer - if g.Server == "" { return errors.New("Groundwork endpoint\\username\\password are not provided ") } @@ -69,8 +92,6 @@ func (g *GW8) Connect() error { return err } - g.writer = io.MultiWriter(writers...) - return nil } @@ -95,7 +116,7 @@ func (g *GW8) Close() error { func (g *GW8) Write(metrics []telegraf.Metric) error { resourceToServicesMap := make(map[string][]addons.DynamicMonitoredService) for _, metric := range metrics { - resource, service := parseMetric(g.DefaultHost, metric) + resource, service := parseMetric(g.DefaultHost, g.DefaultServiceState, g.ResourceTag, metric) resourceToServicesMap[resource] = append(resourceToServicesMap[resource], service) } @@ -105,8 +126,7 @@ func (g *GW8) Write(metrics []telegraf.Metric) error { Name: resourceName, Type: addons.Host, Status: addons.HostUp, - LastCheckTime: addons.MillisecondTimestamp{Time: time.Now()}, - NextCheckTime: addons.MillisecondTimestamp{Time: time.Now()}, + LastCheckTime: &addons.Timestamp{Time: time.Now()}, Services: services, }) } @@ -115,8 +135,8 @@ func (g *GW8) Write(metrics []telegraf.Metric) error { Context: &addons.TracerContext{ AppType: g.AppType, AgentID: g.AgentId, - TraceToken: "cd05607a-4d23-4338-95db-c96367034d23", - TimeStamp: addons.MillisecondTimestamp{Time: time.Now()}, + TraceToken: makeTracerToken(), + TimeStamp: &addons.Timestamp{Time: time.Now()}, Version: addons.ModelVersion, }, Resources: resources, @@ -134,19 +154,38 @@ func (g *GW8) Write(metrics []telegraf.Metric) error { "Accept": "application/json", } - if _, _, err = addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJson); err != nil { - return err + statusCode, _, httpErr := addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJson) + if err != nil { + return httpErr + } + + /* Re-login mechanism */ + if statusCode == 401 { + if err = g.Connect(); err != nil { + return err + } + headers["GWOS-API-TOKEN"] = g.authToken + statusCode, body, httpErr := addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJson) + if httpErr != nil { + return httpErr + } + if statusCode != 200 { + return errors.New(fmt.Sprintf("something went wrong during processing an http request[http_status = %d, body = %s]", statusCode, string(body))) + } } return nil } -func parseMetric(hostname string, metric telegraf.Metric) (string, addons.DynamicMonitoredService) { +func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metric telegraf.Metric) (string, addons.DynamicMonitoredService) { resource := "default_telegraf" - if hostname != "" { - resource = hostname + if defaultHostname != "" { + resource = defaultHostname } - if value, present := metric.GetTag("resource"); present { + if resourceTag == "" { + resourceTag = "host" + } + if value, present := metric.GetTag(resourceTag); present { resource = value } @@ -156,6 +195,9 @@ func parseMetric(hostname string, metric telegraf.Metric) (string, addons.Dynami } status := string(addons.ServiceOk) + if defaultServiceState != "" && validStatus(defaultServiceState) { + status = defaultServiceState + } if value, present := metric.GetTag("status"); present { if validStatus(value) { status = value @@ -177,7 +219,6 @@ func parseMetric(hostname string, metric telegraf.Metric) (string, addons.Dynami if s, err := strconv.ParseFloat(value, 64); err == nil { critical = s } - unitType = value } warning := -1.0 @@ -185,16 +226,14 @@ func parseMetric(hostname string, metric telegraf.Metric) (string, addons.Dynami if s, err := strconv.ParseFloat(value, 64); err == nil { warning = s } - unitType = value } serviceObject := addons.DynamicMonitoredService{ Name: service, - Type: "Service", + Type: addons.Service, Owner: resource, Status: addons.MonitorStatus(status), - LastCheckTime: addons.MillisecondTimestamp{Time: time.Now()}, - NextCheckTime: addons.MillisecondTimestamp{Time: time.Now()}, + LastCheckTime: &addons.Timestamp{Time: metric.Time()}, LastPlugInOutput: message, Metrics: nil, } @@ -218,24 +257,13 @@ func parseMetric(hostname string, metric telegraf.Metric) (string, addons.Dynami }, }) - var val float64 - switch i := value.Value.(type) { - case float64: - val = i - case float32: - val = float64(i) - case int64: - val = float64(i) - case int: - val = float64(i) - default: - } + val, _ := internal.ToFloat64(value.Value) serviceObject.Metrics = append(serviceObject.Metrics, addons.TimeSeries{ MetricName: value.Key, SampleType: addons.Value, Interval: &addons.TimeInterval{ - EndTime: addons.MillisecondTimestamp{Time: time.Now()}, - StartTime: addons.MillisecondTimestamp{Time: time.Now()}, + EndTime: &addons.Timestamp{Time: time.Now()}, + StartTime: &addons.Timestamp{Time: time.Now()}, }, Value: &addons.TypedValue{ ValueType: addons.DoubleType, @@ -316,8 +344,6 @@ func calculateStatus(value *addons.TypedValue, warning *addons.TypedValue, criti if warning != nil { switch warning.ValueType { - case addons.IntegerType: - warningValue = float64(warning.IntegerValue) case addons.DoubleType: warningValue = warning.DoubleValue } @@ -325,48 +351,12 @@ func calculateStatus(value *addons.TypedValue, warning *addons.TypedValue, criti if critical != nil { switch critical.ValueType { - case addons.IntegerType: - criticalValue = float64(critical.IntegerValue) case addons.DoubleType: criticalValue = critical.DoubleValue } } switch value.ValueType { - case addons.IntegerType: - if warning == nil && criticalValue == -1 { - if float64(value.IntegerValue) >= criticalValue { - return addons.ServiceUnscheduledCritical - } - return addons.ServiceOk - } - if critical == nil && (warning != nil && warningValue == -1) { - if float64(value.IntegerValue) >= warningValue { - return addons.ServiceWarning - } - return addons.ServiceOk - } - if (warning != nil && warningValue == -1) && (critical != nil && criticalValue == -1) { - return addons.ServiceOk - } - // is it a reverse comparison (low to high) - if (warning != nil && critical != nil) && warningValue > criticalValue { - if float64(value.IntegerValue) <= criticalValue { - return addons.ServiceUnscheduledCritical - } - if float64(value.IntegerValue) <= warningValue { - return addons.ServiceWarning - } - return addons.ServiceOk - } else { - if (warning != nil && critical != nil) && float64(value.IntegerValue) >= criticalValue { - return addons.ServiceUnscheduledCritical - } - if (warning != nil && critical != nil) && float64(value.IntegerValue) >= warningValue { - return addons.ServiceWarning - } - return addons.ServiceOk - } case addons.DoubleType: if warning == nil && criticalValue == -1 { if value.DoubleValue >= criticalValue { @@ -414,6 +404,38 @@ func validStatus(status string) bool { status == string(addons.ServiceUnknown) } +// makeTracerContext +func makeTracerToken() string { + tracerOnce.Do(initTracerToken) + + /* combine TraceToken from fixed and incremental parts */ + tokenBuf := make([]byte, 16) + copy(tokenBuf, tracerToken) + if tokenInc, err := tracerCache.IncrementUint64(cacheKeyTracerToken, 1); err == nil { + binary.PutUvarint(tokenBuf, tokenInc) + } else { + /* fallback with timestamp */ + binary.PutVarint(tokenBuf, time.Now().UnixNano()) + } + traceToken, _ := uuid.FormatUUID(tokenBuf) + + return traceToken +} + +func initTracerToken() { + /* prepare random tracerToken */ + token := []byte("aaaabbbbccccdddd") + if randBuf, err := uuid.GenerateRandomBytes(16); err == nil { + copy(tracerToken, randBuf) + } else { + /* fallback with multiplied timestamp */ + binary.PutVarint(tracerToken, time.Now().UnixNano()) + binary.PutVarint(tracerToken[6:], time.Now().UnixNano()) + } + tracerCache.Set(cacheKeyTracerToken, uint64(1), -1) + tracerToken = token +} + func init() { outputs.Add("gw8", func() telegraf.Output { return &GW8{} diff --git a/plugins/outputs/gw8/gw8_test.go b/plugins/outputs/gw8/gw8_test.go index 051bea581a5da..0ceba82a8c631 100644 --- a/plugins/outputs/gw8/gw8_test.go +++ b/plugins/outputs/gw8/gw8_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/testutil" "github.com/stretchr/testify/assert" "io/ioutil" @@ -22,6 +23,13 @@ func TestWrite(t *testing.T) { // Generate test metric with default name to test Write logic metric := testutil.TestMetric(1, defaultTestServiceName) + // Get value from metric to compare with value from json result + expectedValue := 0.0 + for _, value := range metric.FieldList() { + expectedValue, _ = internal.ToFloat64(value.Value) + break + } + // 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) @@ -41,6 +49,11 @@ func TestWrite(t *testing.T) { obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[0].(map[string]interface{})["name"], defaultTestServiceName, ) + assert.Equal( + t, + obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[0].(map[string]interface{})["metrics"].([]interface{})[0].(map[string]interface{})["value"].(map[string]interface{})["doubleValue"].(float64), + expectedValue, + ) _, err = fmt.Fprintln(w, `OK`) assert.NoError(t, err) From 3089604816ef29f9164ed31c82a98bcb93856020 Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Tue, 12 Oct 2021 16:35:03 +0300 Subject: [PATCH 03/11] feat: fix timestamp, sampleConfig; add string value for metric parser --- plugins/outputs/gw8/README.md | 12 +- plugins/outputs/gw8/addons/milliseconds.go | 159 --------------------- plugins/outputs/gw8/addons/timestamp.go | 46 ++++++ plugins/outputs/gw8/addons/types.go | 13 +- plugins/outputs/gw8/gw8.go | 108 +++++++------- plugins/outputs/gw8/gw8_test.go | 29 ++-- 6 files changed, 128 insertions(+), 239 deletions(-) delete mode 100644 plugins/outputs/gw8/addons/milliseconds.go create mode 100644 plugins/outputs/gw8/addons/timestamp.go diff --git a/plugins/outputs/gw8/README.md b/plugins/outputs/gw8/README.md index aa98731e26d02..9f01dbc134937 100644 --- a/plugins/outputs/gw8/README.md +++ b/plugins/outputs/gw8/README.md @@ -9,7 +9,7 @@ This plugin writes to a Groundwork instance using "[GROUNDWORK][]" own format. ```toml [[outputs.gw8]] ## HTTP endpoint for your groundwork instance. - groundwork_endpoint = "" + endpoint = "" ## Agent uuid for Groundwork API Server agent_id = "" @@ -19,6 +19,16 @@ This plugin writes to a Groundwork instance using "[GROUNDWORK][]" own format. ## Username to access Groundwork API username = "" + ## Password to use in pair with username password = "" + + ## Default display name for the host with services(metrics) + default_host = "default_telegraf" + + ## Default service state [default - "SERVICE_OK"] + default_service_state = "SERVICE_OK" + + ## The name of the tag that contains the hostname [default - "host"] + resource_tag = "host" ``` \ No newline at end of file diff --git a/plugins/outputs/gw8/addons/milliseconds.go b/plugins/outputs/gw8/addons/milliseconds.go deleted file mode 100644 index dcf162f03a414..0000000000000 --- a/plugins/outputs/gw8/addons/milliseconds.go +++ /dev/null @@ -1,159 +0,0 @@ -package addons - -import ( - "bytes" - "strconv" - "time" -) - -// Timestamp wraps time.Time and adapts json.Marshaler. -// -// Timestamp refers to the JSON representation of timestamps, for -// time-data interchange, as a single integer representing a modified version of -// whole milliseconds since the UNIX epoch (00:00:00 UTC on January 1, 1970). -// Individual languages (Go, C, Java) will typically implement this structure -// using a more-complex construction in their respective contexts, containing even -// finer granularity for local data storage, typically at the nanosecond level. -// -// The "modified version" comment reflects the following simplification. -// Despite the already fine-grained representation as milliseconds, this data -// value takes no account of leap seconds; for all of our calculations, we -// simply pretend they don't exist. Individual feeders will typically map a -// 00:00:60 value for a leap second, obtained as a string so the presence of the -// leap second is obvious, as 00:01:00, and the fact that 00:01:00 will occur -// again in the following second will be silently ignored. This means that any -// monitoring which really wants to accurately reflect International Atomic Time -// (TAI), UT1, or similar time coordinates will be subject to some disruption. -// It also means that even in ordinary circumstances, any calculations of -// sub-second time differences might run into surprises, since the following -// timestamps could appear in temporal order: -// -// actual time relative reported time in milliseconds -// A: 00:00:59.000 59000 -// B: 00:00:60.000 60000 -// C: 00:00:60.700 60700 -// D: 00:01:00.000 60000 -// E: 00:01:00.300 60300 -// F: 00:01:01.000 61000 -// -// In such a situation, (D - C) and (E - C) would be negative numbers. -// -// In other situations, a feeder might obtain a timestamp from a system hardware -// clock which, say, counts local nanoseconds and has no notion of any leap -// seconds having been inserted into human-readable string-time representations. -// So there could be some amount of offset if such values are compared across -// such a boundary. -// -// Beyond that, there is always the issue of computer clocks not being directly -// tied to atomic clocks, using inexpensive non-temperature-compensated crystals -// for timekeeping. Such hardware can easily drift dramatically off course, and -// the local timekeeping may or may not be subject to course correction using -// HTP, chrony, or similar software that periodically adjusts the system time -// to keep it synchronized with the Internet. Also, there may be large jumps -// in either a positive or negative direction when a drifted clock is suddenly -// brought back into synchronization with the rest of the world. -// -// In addition, we ignore here all temporal effects of Special Relativity, not -// to mention further adjustments needed to account for General Relativity. -// This is not a theoretical joke; those who monitor GPS satellites should take -// note of the limitations of this data type, and use some other data type for -// time-critical data exchange and calculations. -// -// The point of all this being, fine resolution of clock values should never be -// taken too seriously unless one is sure that the clocks being compared are -// directly hitched together, and even then one must allow for quantum leaps -// into the future and time travel into the past. -// -// Finally, note that the Go zero-value of the internal implementation object -// we use in that language does not have a reasonable value when interpreted -// as milliseconds since the UNIX epoch. For that reason, the general rule is -// that the JSON representation of a zero-value for any field of this type, no -// matter what the originating language, will be to simply omit it from the -// JSON string. That fact must be taken into account when marshalling and -// unmarshalling data structures that contain such fields. -// -type Timestamp struct { - time.Time -} - -// NewTimestamp returns reference to new timestamp setted to UTC now. -func NewTimestamp() *Timestamp { - return &Timestamp{time.Now().UTC()} -} - -// Add returns the timestamp t+d. -// Overrides nested time.Time Add. -func (t Timestamp) Add(d time.Duration) Timestamp { - return Timestamp{t.Time.Add(d)} -} - -// AddDate returns the timestamp corresponding to adding the given number of years, months, and days. -// Overrides nested time.Time AddDate. -func (t Timestamp) AddDate(years int, months int, days int) Timestamp { - return Timestamp{t.Time.AddDate(years, months, days)} -} - -// In returns a copy of t with location set to loc. -// Overrides nested time.Time In. -func (t Timestamp) In(loc *time.Location) Timestamp { - return Timestamp{t.Time.In(loc)} -} - -// Local returns a copy of t with the location set to local time. -// Overrides nested time.Time Local. -func (t Timestamp) Local() Timestamp { - return Timestamp{t.Time.Local()} -} - -// Round returns a copy of t rounded to the nearest multiple of d. -// Overrides nested time.Time Round. -func (t Timestamp) Round(d time.Duration) Timestamp { - return Timestamp{t.Time.Round(d)} -} - -// Truncate returns a copy t rounded down to a multiple of d. -// Overrides nested time.Time Truncate. -func (t Timestamp) Truncate(d time.Duration) Timestamp { - return Timestamp{t.Time.Truncate(d)} -} - -// UTC returns a copy of t with the location set to UTC. -// Overrides nested time.Time UTC. -func (t Timestamp) UTC() Timestamp { - return Timestamp{t.Time.UTC()} -} - -// MarshalJSON implements json.Marshaler. -// Overrides nested time.Time MarshalJSON. -func (t Timestamp) MarshalJSON() ([]byte, error) { - i := t.UnixMilli() - buf := make([]byte, 0, 16) - buf = append(buf, '"') - buf = strconv.AppendInt(buf, i, 10) - buf = append(buf, '"') - return buf, nil -} - -// String implements fmt.Stringer. -// Overrides nested time.Time String. -func (t Timestamp) String() string { - i := t.UnixMilli() - buf := make([]byte, 0, 16) - buf = strconv.AppendInt(buf, i, 10) - return string(buf) -} - -// UnmarshalJSON implements json.Unmarshaler. -// Overrides nested time.Time UnmarshalJSON. -func (t *Timestamp) UnmarshalJSON(input []byte) error { - strInput := string(bytes.Trim(input, `"`)) - - i, err := strconv.ParseInt(strInput, 10, 64) - if err != nil { - return err - } - - i *= int64(time.Millisecond) - *t = Timestamp{time.Unix(0, i).UTC()} - return nil -} diff --git a/plugins/outputs/gw8/addons/timestamp.go b/plugins/outputs/gw8/addons/timestamp.go new file mode 100644 index 0000000000000..3a0129a398c04 --- /dev/null +++ b/plugins/outputs/gw8/addons/timestamp.go @@ -0,0 +1,46 @@ +package addons + +import ( + "bytes" + "strconv" + "time" +) + +// Timestamp aliases time.Time and adapts MarshalJSON and UnmarshalJSON +type Timestamp time.Time + +// MarshalJSON implements json.Marshaler. +// Overrides nested time.Time MarshalJSON. +func (t Timestamp) MarshalJSON() ([]byte, error) { + i := time.Time(t).UnixMilli() + buf := make([]byte, 0, 16) + buf = append(buf, '"') + buf = strconv.AppendInt(buf, i, 10) + buf = append(buf, '"') + return buf, nil +} + +// UnmarshalJSON implements json.Unmarshaler. +// Overrides nested time.Time UnmarshalJSON. +func (t *Timestamp) UnmarshalJSON(input []byte) error { + strInput := string(bytes.Trim(input, `"`)) + + i, err := strconv.ParseInt(strInput, 10, 64) + if err != nil { + return err + } + + i *= int64(time.Millisecond) + *t = Timestamp(time.Unix(0, i).UTC()) + return nil +} + +func Now() *Timestamp { + now := Timestamp(time.Now()) + return &now +} + +func TimestampRef(t time.Time) *Timestamp { + ref := Timestamp(t) + return &ref +} diff --git a/plugins/outputs/gw8/addons/types.go b/plugins/outputs/gw8/addons/types.go index bbe9f4a00a8c9..059f484825ff0 100644 --- a/plugins/outputs/gw8/addons/types.go +++ b/plugins/outputs/gw8/addons/types.go @@ -40,6 +40,7 @@ type ValueType string // DoubleType type of the value of a metric const ( DoubleType ValueType = "DoubleType" + StringType ValueType = "StringType" ) type MonitorStatus string @@ -73,23 +74,13 @@ const ( type TypedValue struct { ValueType ValueType `json:"valueType"` - // BoolValue: A Boolean value: true or false. - BoolValue bool `json:"boolValue,omitempty"` - // DoubleValue: A 64-bit double-precision floating-point number. Its // magnitude is approximately ±10±300 and it // has 16 significant digits of precision. DoubleValue float64 `json:"doubleValue,omitempty"` - // Int64Value: A 64-bit integer. Its range is approximately - // ±9.2x1018. - IntegerValue int64 `json:"integerValue,omitempty"` - // StringValue: A variable-length string value. - StringValue string `json:"stringValue,omitempty"` - - // a time stored as full timestamp - TimeValue *Timestamp `json:"timeValue,omitempty"` + StringValue *string `json:"stringValue,omitempty"` } // TimeInterval defines a closed time interval. It extends from the start time diff --git a/plugins/outputs/gw8/gw8.go b/plugins/outputs/gw8/gw8.go index eafb43cb8b18a..82536a6e2b254 100644 --- a/plugins/outputs/gw8/gw8.go +++ b/plugins/outputs/gw8/gw8.go @@ -23,8 +23,8 @@ const ( // Login and logout routes from Groundwork API const ( - loginUrl = "/api/auth/login" - logoutUrl = "/api/auth/logout" + loginURL = "/api/auth/login" + logoutURL = "/api/auth/logout" ) var ( @@ -41,7 +41,7 @@ var ( var ( sampleConfig = ` ## HTTP endpoint for your groundwork instance. - groundwork_endpoint = "" + endpoint = "" ## Agent uuid for Groundwork API Server agent_id = "" @@ -51,23 +51,24 @@ var ( ## Username to access Groundwork API username = "" + ## Password to use in pair with username password = "" ## Default display name for the host with services(metrics) default_host = "default_telegraf" - ## Default service state [default - "host"] + ## Default service state [default - "SERVICE_OK"] default_service_state = "SERVICE_OK" - ## The name of the tag that contains the hostname + ## The name of the tag that contains the hostname [default - "host"] resource_tag = "host" ` ) type GW8 struct { Server string `toml:"groundwork_endpoint"` - AgentId string `toml:"agent_id"` + AgentID string `toml:"agent_id"` AppType string `toml:"app_type"` Username string `toml:"username"` Password string `toml:"password"` @@ -83,16 +84,15 @@ func (g *GW8) SampleConfig() string { func (g *GW8) Connect() error { if g.Server == "" { - return errors.New("Groundwork endpoint\\username\\password are not provided ") + return errors.New("no 'server' provided") } - if byteToken, err := login(g.Server+loginUrl, g.Username, g.Password); err == nil { + byteToken, err := login(g.Server+loginURL, g.Username, g.Password) + if err == nil { g.authToken = string(byteToken) - } else { - return err } - return nil + return err } func (g *GW8) Close() error { @@ -105,12 +105,9 @@ func (g *GW8) Close() error { "Content-Type": "application/x-www-form-urlencoded", } - _, _, err := addons.SendRequest(http.MethodPost, g.Server+logoutUrl, headers, formValues, nil) - if err != nil { - return err - } + _, _, err := addons.SendRequest(http.MethodPost, g.Server+logoutURL, headers, formValues, nil) - return nil + return err } func (g *GW8) Write(metrics []telegraf.Metric) error { @@ -126,17 +123,17 @@ func (g *GW8) Write(metrics []telegraf.Metric) error { Name: resourceName, Type: addons.Host, Status: addons.HostUp, - LastCheckTime: &addons.Timestamp{Time: time.Now()}, + LastCheckTime: addons.Now(), Services: services, }) } - requestJson, err := json.Marshal(addons.DynamicResourcesWithServicesRequest{ + requestJSON, err := json.Marshal(addons.DynamicResourcesWithServicesRequest{ Context: &addons.TracerContext{ AppType: g.AppType, - AgentID: g.AgentId, + AgentID: g.AgentID, TraceToken: makeTracerToken(), - TimeStamp: &addons.Timestamp{Time: time.Now()}, + TimeStamp: addons.Now(), Version: addons.ModelVersion, }, Resources: resources, @@ -154,9 +151,9 @@ func (g *GW8) Write(metrics []telegraf.Metric) error { "Accept": "application/json", } - statusCode, _, httpErr := addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJson) + statusCode, _, err := addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) if err != nil { - return httpErr + return err } /* Re-login mechanism */ @@ -165,12 +162,12 @@ func (g *GW8) Write(metrics []telegraf.Metric) error { return err } headers["GWOS-API-TOKEN"] = g.authToken - statusCode, body, httpErr := addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJson) + statusCode, body, httpErr := addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) if httpErr != nil { return httpErr } if statusCode != 200 { - return errors.New(fmt.Sprintf("something went wrong during processing an http request[http_status = %d, body = %s]", statusCode, string(body))) + return fmt.Errorf("something went wrong during processing an http request[http_status = %d, body = %s]", statusCode, string(body)) } } @@ -233,7 +230,7 @@ func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metri Type: addons.Service, Owner: resource, Status: addons.MonitorStatus(status), - LastCheckTime: &addons.Timestamp{Time: metric.Time()}, + LastCheckTime: addons.Now(), LastPlugInOutput: message, Metrics: nil, } @@ -257,17 +254,28 @@ func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metri }, }) - val, _ := internal.ToFloat64(value.Value) + valueType := addons.DoubleType + var floatVal float64 + var stringVal *string + + switch value.Value.(type) { + case string: + valueType = addons.StringType + tmpStr := value.Value.(string) + stringVal = &tmpStr + default: + floatVal, _ = internal.ToFloat64(value.Value) + } serviceObject.Metrics = append(serviceObject.Metrics, addons.TimeSeries{ MetricName: value.Key, SampleType: addons.Value, Interval: &addons.TimeInterval{ - EndTime: &addons.Timestamp{Time: time.Now()}, - StartTime: &addons.Timestamp{Time: time.Now()}, + EndTime: addons.TimestampRef(metric.Time()), }, Value: &addons.TypedValue{ - ValueType: addons.DoubleType, - DoubleValue: val, + ValueType: valueType, + DoubleValue: floatVal, + StringValue: stringVal, }, Unit: addons.UnitType(unitType), Thresholds: &thresholds, @@ -299,8 +307,7 @@ func login(url, username, password string) ([]byte, error) { return nil, err } if statusCode != 200 { - return nil, errors.New(fmt.Sprintf("[ERROR]: Http request failed. [Status code]: %d, [Response]: %s", - statusCode, string(body))) + return nil, fmt.Errorf("[ERROR]: Http request failed. [Status code]: %d, [Response]: %s", statusCode, string(body)) } return body, nil @@ -343,21 +350,18 @@ func calculateStatus(value *addons.TypedValue, warning *addons.TypedValue, criti var criticalValue float64 if warning != nil { - switch warning.ValueType { - case addons.DoubleType: + if warning.ValueType == addons.DoubleType { warningValue = warning.DoubleValue } } if critical != nil { - switch critical.ValueType { - case addons.DoubleType: + if critical.ValueType == addons.DoubleType { criticalValue = critical.DoubleValue } } - switch value.ValueType { - case addons.DoubleType: + if value.ValueType == addons.DoubleType { if warning == nil && criticalValue == -1 { if value.DoubleValue >= criticalValue { return addons.ServiceUnscheduledCritical @@ -382,26 +386,26 @@ func calculateStatus(value *addons.TypedValue, warning *addons.TypedValue, criti return addons.ServiceWarning } return addons.ServiceOk - } else { - if value.DoubleValue >= criticalValue { - return addons.ServiceUnscheduledCritical - } - if value.DoubleValue >= warningValue { - return addons.ServiceWarning - } - return addons.ServiceOk } + if value.DoubleValue >= criticalValue { + return addons.ServiceUnscheduledCritical + } + if value.DoubleValue >= warningValue { + return addons.ServiceWarning + } + + return addons.ServiceOk } return addons.ServiceOk } func validStatus(status string) bool { - return status == string(addons.ServiceOk) || - status == string(addons.ServiceWarning) || - status == string(addons.ServicePending) || - status == string(addons.ServiceScheduledCritical) || - status == string(addons.ServiceUnscheduledCritical) || - status == string(addons.ServiceUnknown) + switch addons.MonitorStatus(status) { + case addons.ServiceOk, addons.ServiceWarning, addons.ServicePending, addons.ServiceScheduledCritical, + addons.ServiceUnscheduledCritical, addons.ServiceUnknown: + return true + } + return false } // makeTracerContext diff --git a/plugins/outputs/gw8/gw8_test.go b/plugins/outputs/gw8/gw8_test.go index 0ceba82a8c631..39d1d295a7405 100644 --- a/plugins/outputs/gw8/gw8_test.go +++ b/plugins/outputs/gw8/gw8_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "github.com/influxdata/telegraf" - "github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/testutil" "github.com/stretchr/testify/assert" "io/ioutil" @@ -14,21 +13,14 @@ import ( ) const ( - defaultTestAgentId = "ec1676cc-583d-48ee-b035-7fb5ed0fcf88" + defaultTestAgentID = "ec1676cc-583d-48ee-b035-7fb5ed0fcf88" defaultTestAppType = "TELEGRAF" - defaultTestServiceName = "GROUNDWORK_TEST" ) func TestWrite(t *testing.T) { // Generate test metric with default name to test Write logic - metric := testutil.TestMetric(1, defaultTestServiceName) - - // Get value from metric to compare with value from json result - expectedValue := 0.0 - for _, value := range metric.FieldList() { - expectedValue, _ = internal.ToFloat64(value.Value) - break - } + floatMetric := testutil.TestMetric(1, "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) { @@ -42,17 +34,22 @@ func TestWrite(t *testing.T) { // Check if server gets valid metrics object assert.Equal(t, obj["context"].(map[string]interface{})["appType"], defaultTestAppType) - assert.Equal(t, obj["context"].(map[string]interface{})["agentId"], defaultTestAgentId) + assert.Equal(t, obj["context"].(map[string]interface{})["agentId"], defaultTestAgentID) assert.Equal(t, obj["resources"].([]interface{})[0].(map[string]interface{})["name"], "default_telegraf") assert.Equal( t, obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[0].(map[string]interface{})["name"], - defaultTestServiceName, + "Float", ) assert.Equal( t, obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[0].(map[string]interface{})["metrics"].([]interface{})[0].(map[string]interface{})["value"].(map[string]interface{})["doubleValue"].(float64), - expectedValue, + 1.0, + ) + assert.Equal( + t, + obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[1].(map[string]interface{})["metrics"].([]interface{})[0].(map[string]interface{})["value"].(map[string]interface{})["stringValue"].(string), + "Test", ) _, err = fmt.Fprintln(w, `OK`) @@ -62,10 +59,10 @@ func TestWrite(t *testing.T) { i := GW8{ Server: server.URL, AppType: defaultTestAppType, - AgentId: defaultTestAgentId, + AgentID: defaultTestAgentID, } - err := i.Write([]telegraf.Metric{metric}) + err := i.Write([]telegraf.Metric{floatMetric, stringMetric}) assert.NoError(t, err) defer server.Close() From 51f1c358a70bbaa54996a56e1dfaa21340e180bd Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Tue, 23 Nov 2021 15:00:22 +0300 Subject: [PATCH 04/11] feat: rewrite groundwork plugin using the gwos/tcg library to make the code shorter --- go.mod | 2 + go.sum | 2 + plugins/outputs/all/all.go | 2 +- plugins/outputs/{gw8 => groundwork}/README.md | 18 +- plugins/outputs/groundwork/groundwork.go | 336 +++++++++++++ plugins/outputs/groundwork/groundwork_test.go | 86 ++++ plugins/outputs/gw8/addons/request.go | 66 --- plugins/outputs/gw8/addons/timestamp.go | 46 -- plugins/outputs/gw8/addons/types.go | 248 ---------- plugins/outputs/gw8/gw8.go | 447 ------------------ plugins/outputs/gw8/gw8_test.go | 71 --- 11 files changed, 435 insertions(+), 889 deletions(-) rename plugins/outputs/{gw8 => groundwork}/README.md (60%) create mode 100644 plugins/outputs/groundwork/groundwork.go create mode 100644 plugins/outputs/groundwork/groundwork_test.go delete mode 100644 plugins/outputs/gw8/addons/request.go delete mode 100644 plugins/outputs/gw8/addons/timestamp.go delete mode 100644 plugins/outputs/gw8/addons/types.go delete mode 100644 plugins/outputs/gw8/gw8.go delete mode 100644 plugins/outputs/gw8/gw8_test.go diff --git a/go.mod b/go.mod index 40880b76ebf05..aae3a3e8fb7af 100644 --- a/go.mod +++ b/go.mod @@ -331,6 +331,8 @@ require ( require github.com/libp2p/go-reuseport v0.1.0 +require github.com/gwos/tcg v0.0.0-20211123085413-fa76511f6546 + require ( github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.2.0 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.4.0 // indirect diff --git a/go.sum b/go.sum index d33788aa5dd21..df31dc99552be 100644 --- a/go.sum +++ b/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 v0.0.0-20211123085413-fa76511f6546 h1:qq2iGm+3HvHr2w3asvxxSxDxjmwBEVJ2vy1zCLp0ZOo= +github.com/gwos/tcg v0.0.0-20211123085413-fa76511f6546/go.mod h1:C7Y3c2iI8mc+VKHaNum0dzP0iWnljEnp8bZ9hrBLK18= 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= diff --git a/plugins/outputs/all/all.go b/plugins/outputs/all/all.go index b3648ef22e0cb..ff23a060b51cc 100644 --- a/plugins/outputs/all/all.go +++ b/plugins/outputs/all/all.go @@ -21,7 +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/gw8" + _ "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" diff --git a/plugins/outputs/gw8/README.md b/plugins/outputs/groundwork/README.md similarity index 60% rename from plugins/outputs/gw8/README.md rename to plugins/outputs/groundwork/README.md index 9f01dbc134937..3e54b40697d44 100644 --- a/plugins/outputs/gw8/README.md +++ b/plugins/outputs/groundwork/README.md @@ -1,34 +1,32 @@ # Groundwork Output Plugin -This plugin writes to a Groundwork instance using "[GROUNDWORK][]" own format. +This plugin writes to a [GroundWork Monitor][1] instance. -[GROUNDWORK]: https://www.gwos.com +[1]: https://www.gwos.com/product/groundwork-monitor/ ### Configuration: ```toml -[[outputs.gw8]] +[[outputs.groundwork]] ## HTTP endpoint for your groundwork instance. - endpoint = "" + groundwork_endpoint = "" ## Agent uuid for Groundwork API Server agent_id = "" - ## Groundwork application type - app_type = "" - ## Username to access Groundwork API username = "" ## Password to use in pair with username password = "" - ## Default display name for the host with services(metrics) - default_host = "default_telegraf" + ## Default display name for the host with services(metrics) [default - "telegraf"] + default_host = "telegraf" ## Default service state [default - "SERVICE_OK"] default_service_state = "SERVICE_OK" ## The name of the tag that contains the hostname [default - "host"] resource_tag = "host" -``` \ No newline at end of file +``` +#### NOTE: Plugin only supports GW8+ \ No newline at end of file diff --git a/plugins/outputs/groundwork/groundwork.go b/plugins/outputs/groundwork/groundwork.go new file mode 100644 index 0000000000000..3205d39ae79ee --- /dev/null +++ b/plugins/outputs/groundwork/groundwork.go @@ -0,0 +1,336 @@ +package groundwork + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/gwos/tcg/clients" + "github.com/gwos/tcg/milliseconds" + "github.com/gwos/tcg/transit" + "github.com/hashicorp/go-uuid" + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/internal" + "github.com/influxdata/telegraf/plugins/outputs" + "net/http" + "strconv" + "time" +) + +const ( + defaultMonitoringRoute = "/api/monitoring?dynamic=true" +) + +// Login and logout routes from Groundwork API +const ( + loginURL = "/api/auth/login" + logoutURL = "/api/auth/logout" +) + +var ( + sampleConfig = ` + ## HTTP endpoint for your groundwork instance. + endpoint = "" + + ## Agent uuid for Groundwork API Server + agent_id = "" + + ## Groundwork application type + app_type = "" + + ## Username to access Groundwork API + username = "" + + ## Password to use in pair with username + password = "" + + ## Default display name for the host with services(metrics) + default_host = "default_telegraf" + + ## Default service state [default - "SERVICE_OK"] + default_service_state = "SERVICE_OK" + + ## The name of the tag that contains the hostname [default - "host"] + resource_tag = "host" +` +) + +type Groundwork struct { + Server string `toml:"groundwork_endpoint"` + 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"` + authToken string +} + +func (g *Groundwork) SampleConfig() string { + return sampleConfig +} + +func (g *Groundwork) Init() error { + if g.Server == "" { + return errors.New("no 'groundwork_endpoint' provided") + } + if g.Username == "" { + return errors.New("no 'username' provided") + } + if g.Password == "" { + return errors.New("no 'password' provided") + } + if g.DefaultHost == "" { + g.DefaultHost = "telegraf" + } + if g.ResourceTag == "" { + g.ResourceTag = "host" + } + if g.DefaultServiceState == "" || !validStatus(g.DefaultServiceState) { + g.DefaultServiceState = string(transit.ServiceOk) + } + + return nil +} + +func (g *Groundwork) Connect() error { + byteToken, err := login(g.Server+loginURL, g.Username, g.Password) + if err == nil { + g.authToken = string(byteToken) + } + + return err +} + +func (g *Groundwork) Close() error { + formValues := map[string]string{ + "gwos-app-name": "telegraf", + "gwos-api-token": g.authToken, + } + + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + + _, _, err := clients.SendRequest(http.MethodPost, g.Server+logoutURL, headers, formValues, nil) + + return err +} + +func (g *Groundwork) Write(metrics []telegraf.Metric) error { + resourceToServicesMap := make(map[string][]transit.DynamicMonitoredService) + for _, metric := range metrics { + resource, service := parseMetric(g.DefaultHost, g.DefaultServiceState, g.ResourceTag, metric) + 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: milliseconds.MillisecondTimestamp{Time: time.Now()}, + NextCheckTime: milliseconds.MillisecondTimestamp{Time: time.Now()}, + Services: services, + }) + } + + traceToken, _ := uuid.GenerateUUID() + requestJSON, err := json.Marshal(transit.DynamicResourcesWithServicesRequest{ + Context: &transit.TracerContext{ + AppType: "TELEGRAF", + AgentID: g.AgentID, + TraceToken: traceToken, + TimeStamp: milliseconds.MillisecondTimestamp{Time: time.Now()}, + Version: transit.ModelVersion, + }, + Resources: resources, + Groups: nil, + }) + + if err != nil { + return err + } + + headers := map[string]string{ + "GWOS-APP-NAME": "groundwork", + "GWOS-API-TOKEN": g.authToken, + "Content-Type": "application/json", + "Accept": "application/json", + } + + statusCode, _, err := clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) + if err != nil { + return err + } + + /* Re-login mechanism */ + if statusCode == 401 { + if err = g.Connect(); err != nil { + return err + } + headers["GWOS-API-TOKEN"] = g.authToken + statusCode, body, httpErr := clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) + if httpErr != nil { + return httpErr + } + if statusCode != 200 { + return fmt.Errorf("something went wrong during processing an http request[http_status = %d, body = %s]", statusCode, string(body)) + } + } + + return nil +} + +func (g *Groundwork) Description() string { + return "Send telegraf metrics to GroundWork Monitor" +} + +func init() { + outputs.Add("groundwork", func() telegraf.Output { + return &Groundwork{} + }) +} + +func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metric telegraf.Metric) (string, transit.DynamicMonitoredService) { + resource := defaultHostname + + if value, present := metric.GetTag(resourceTag); present { + resource = value + } + + service := metric.Name() + if value, present := metric.GetTag("service"); present { + service = value + } + + status := defaultServiceState + if value, present := metric.GetTag("status"); present { + if validStatus(value) { + status = value + } + } + + message := "" + if value, present := metric.GetTag("message"); present { + message = value + } + + unitType := string(transit.UnitCounter) + if value, present := metric.GetTag("unitType"); present { + unitType = value + } + + critical := -1.0 + if value, present := metric.GetTag("critical"); present { + if s, err := strconv.ParseFloat(value, 64); err == nil { + critical = s + } + } + + warning := -1.0 + if value, present := metric.GetTag("warning"); present { + if s, err := strconv.ParseFloat(value, 64); err == nil { + warning = s + } + } + + serviceObject := transit.DynamicMonitoredService{ + BaseTransitData: transit.BaseTransitData{ + Name: service, + Type: transit.Service, + Owner: resource, + }, + Status: transit.MonitorStatus(status), + LastCheckTime: milliseconds.MillisecondTimestamp{Time: time.Now()}, + NextCheckTime: milliseconds.MillisecondTimestamp{}, + LastPlugInOutput: message, + Metrics: nil, + } + + for _, value := range metric.FieldList() { + var thresholds []transit.ThresholdValue + thresholds = append(thresholds, transit.ThresholdValue{ + SampleType: transit.Warning, + Label: value.Key + "_wn", + Value: &transit.TypedValue{ + ValueType: transit.DoubleType, + DoubleValue: warning, + }, + }) + thresholds = append(thresholds, transit.ThresholdValue{ + SampleType: transit.Critical, + Label: value.Key + "_cr", + Value: &transit.TypedValue{ + ValueType: transit.DoubleType, + DoubleValue: critical, + }, + }) + + valueType := transit.DoubleType + var floatVal float64 + var stringVal string + + switch value.Value.(type) { + case string: + valueType = transit.StringType + tmpStr := value.Value.(string) + stringVal = tmpStr + default: + floatVal, _ = internal.ToFloat64(value.Value) + } + serviceObject.Metrics = append(serviceObject.Metrics, transit.TimeSeries{ + MetricName: value.Key, + SampleType: transit.Value, + Interval: &transit.TimeInterval{ + EndTime: milliseconds.MillisecondTimestamp{Time: metric.Time()}, + }, + Value: &transit.TypedValue{ + ValueType: valueType, + DoubleValue: floatVal, + StringValue: stringVal, + }, + Unit: transit.UnitType(unitType), + Thresholds: &thresholds, + }) + } + + serviceObject.Status, _ = transit.CalculateServiceStatus(&serviceObject.Metrics) + + return resource, serviceObject +} + +func login(url, username, password string) ([]byte, error) { + formValues := map[string]string{ + "user": username, + "password": password, + "gwos-app-name": "groundwork", + } + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "text/plain", + } + + statusCode, body, err := clients.SendRequest(http.MethodPost, url, headers, formValues, nil) + if err != nil { + return nil, err + } + if statusCode != 200 { + return nil, fmt.Errorf("[ERROR]: Http request failed. [Status code]: %d, [Response]: %s", statusCode, string(body)) + } + + return body, 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 +} diff --git a/plugins/outputs/groundwork/groundwork_test.go b/plugins/outputs/groundwork/groundwork_test.go new file mode 100644 index 0000000000000..28b24339dd05d --- /dev/null +++ b/plugins/outputs/groundwork/groundwork_test.go @@ -0,0 +1,86 @@ +package groundwork + +import ( + "encoding/json" + "fmt" + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/testutil" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +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, "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) + assert.NoError(t, err) + + // Decode body to use in assertations below + var obj GroundworkObject + err = json.Unmarshal(body, &obj) + assert.NoError(t, err) + + // Check if server gets valid metrics object + assert.Equal(t, obj.Context.AgentID, defaultTestAgentID) + assert.Equal(t, obj.Resources[0].Name, defaultHost) + assert.Equal( + t, + obj.Resources[0].Services[0].Name, + "Float", + ) + assert.Equal( + t, + obj.Resources[0].Services[0].Metrics[0].Value.DoubleValue, + 1.0, + ) + assert.Equal( + t, + obj.Resources[0].Services[1].Metrics[0].Value.StringValue, + "Test", + ) + + _, err = fmt.Fprintln(w, `OK`) + assert.NoError(t, err) + })) + + i := Groundwork{ + Server: server.URL, + AgentID: defaultTestAgentID, + DefaultHost: "telegraf", + } + + err := i.Write([]telegraf.Metric{floatMetric, stringMetric}) + assert.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"` +} diff --git a/plugins/outputs/gw8/addons/request.go b/plugins/outputs/gw8/addons/request.go deleted file mode 100644 index afe45b71dbe3b..0000000000000 --- a/plugins/outputs/gw8/addons/request.go +++ /dev/null @@ -1,66 +0,0 @@ -package addons - -import ( - "bytes" - "crypto/tls" - "io" - "io/ioutil" - "net/http" - "net/url" -) - -var httpClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, -} - -// SendRequest wraps HTTP methods -func SendRequest(httpMethod string, requestURL string, - headers map[string]string, formValues map[string]string, byteBody []byte) (int, []byte, error) { - - var request *http.Request - var response *http.Response - var err error - - urlValues := url.Values{} - if formValues != nil { - for key, value := range formValues { - urlValues.Add(key, value) - } - byteBody = []byte(urlValues.Encode()) - } - - var body io.Reader - if byteBody != nil { - body = bytes.NewBuffer(byteBody) - } else { - body = nil - } - - request, err = http.NewRequest(httpMethod, requestURL, body) - - if err != nil { - return -1, nil, err - } - - request.Header.Set("Connection", "close") - if headers != nil { - for key, value := range headers { - request.Header.Add(key, value) - } - } - - response, err = httpClient.Do(request) - if err != nil { - return -1, nil, err - } - - defer response.Body.Close() - - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return -1, nil, err - } - return response.StatusCode, responseBody, nil -} diff --git a/plugins/outputs/gw8/addons/timestamp.go b/plugins/outputs/gw8/addons/timestamp.go deleted file mode 100644 index 3a0129a398c04..0000000000000 --- a/plugins/outputs/gw8/addons/timestamp.go +++ /dev/null @@ -1,46 +0,0 @@ -package addons - -import ( - "bytes" - "strconv" - "time" -) - -// Timestamp aliases time.Time and adapts MarshalJSON and UnmarshalJSON -type Timestamp time.Time - -// MarshalJSON implements json.Marshaler. -// Overrides nested time.Time MarshalJSON. -func (t Timestamp) MarshalJSON() ([]byte, error) { - i := time.Time(t).UnixMilli() - buf := make([]byte, 0, 16) - buf = append(buf, '"') - buf = strconv.AppendInt(buf, i, 10) - buf = append(buf, '"') - return buf, nil -} - -// UnmarshalJSON implements json.Unmarshaler. -// Overrides nested time.Time UnmarshalJSON. -func (t *Timestamp) UnmarshalJSON(input []byte) error { - strInput := string(bytes.Trim(input, `"`)) - - i, err := strconv.ParseInt(strInput, 10, 64) - if err != nil { - return err - } - - i *= int64(time.Millisecond) - *t = Timestamp(time.Unix(0, i).UTC()) - return nil -} - -func Now() *Timestamp { - now := Timestamp(time.Now()) - return &now -} - -func TimestampRef(t time.Time) *Timestamp { - ref := Timestamp(t) - return &ref -} diff --git a/plugins/outputs/gw8/addons/types.go b/plugins/outputs/gw8/addons/types.go deleted file mode 100644 index 059f484825ff0..0000000000000 --- a/plugins/outputs/gw8/addons/types.go +++ /dev/null @@ -1,248 +0,0 @@ -package addons - -// MonitorStatusWeightService defines weight of Monitor Status for multi-state comparison -var MonitorStatusWeightService = map[MonitorStatus]int{ - ServiceOk: 0, - ServicePending: 10, - ServiceUnknown: 20, - ServiceWarning: 30, - ServiceScheduledCritical: 50, - ServiceUnscheduledCritical: 100, -} - -// MetricSampleType defines TimeSeries Metric Sample Possible Types -type MetricSampleType string - -// TimeSeries Metric Sample Possible Types -const ( - Value MetricSampleType = "Value" - Warning = "Warning" - Critical = "Critical" -) - -// UnitType - Supported units are a subset of The Unified Code for Units of Measure -// (http://unitsofmeasure.org/ucum.html) standard, added as we encounter -// the need for them in monitoring contexts. -type UnitType string - -// Supported units -const ( - UnitCounter UnitType = "1" - PercentCPU = "%{cpu}" - KB = "KB" - MB = "MB" - GB = "GB" -) - -// ValueType defines the data type of the value of a metric -type ValueType string - -// DoubleType type of the value of a metric -const ( - DoubleType ValueType = "DoubleType" - StringType ValueType = "StringType" -) - -type MonitorStatus string - -const ( - ServiceOk MonitorStatus = "SERVICE_OK" - ServiceWarning MonitorStatus = "SERVICE_WARNING" - ServiceUnscheduledCritical MonitorStatus = "SERVICE_UNSCHEDULED_CRITICAL" - ServicePending MonitorStatus = "SERVICE_PENDING" - ServiceScheduledCritical MonitorStatus = "SERVICE_SCHEDULED_CRITICAL" - ServiceUnknown MonitorStatus = "SERVICE_UNKNOWN" - HostUp MonitorStatus = "HOST_UP" - HostUnscheduledDown MonitorStatus = "HOST_UNSCHEDULED_DOWN" - HostPending MonitorStatus = "HOST_PENDING" - HostScheduledDown MonitorStatus = "HOST_SCHEDULED_DOWN" - HostUnreachable MonitorStatus = "HOST_UNREACHABLE" - HostUnchanged MonitorStatus = "HOST_UNCHANGED" -) - -// ResourceType defines the resource type -type ResourceType string - -// Host The resource type uniquely defining the resource type -// General Nagios Types are host and service, whereas CloudHub can have richer complexity -const ( - Host ResourceType = "host" - Service = "service" -) - -// TypedValue defines a single strongly-typed value. -type TypedValue struct { - ValueType ValueType `json:"valueType"` - - // DoubleValue: A 64-bit double-precision floating-point number. Its - // magnitude is approximately ±10±300 and it - // has 16 significant digits of precision. - DoubleValue float64 `json:"doubleValue,omitempty"` - - // StringValue: A variable-length string value. - StringValue *string `json:"stringValue,omitempty"` -} - -// TimeInterval defines a closed time interval. It extends from the start time -// to the end time, and includes both: [startTime, endTime]. Valid time -// intervals depend on the MetricKind of the metric value. In no case -// can the end time be earlier than the start time. -// For a GAUGE metric, the StartTime value is technically optional; if -// no value is specified, the start time defaults to the value of the -// end time, and the interval represents a single point in time. Such an -// interval is valid only for GAUGE metrics, which are point-in-time -// measurements. -// For DELTA and CUMULATIVE metrics, the start time must be earlier -// than the end time. -// In all cases, the start time of the next interval must be at least a -// microsecond after the end time of the previous interval. Because the -// interval is closed, if the start time of a new interval is the same -// as the end time of the previous interval, data written at the new -// start time could overwrite data written at the previous end time. -type TimeInterval struct { - // EndTime: Required. The end of the time interval. - EndTime *Timestamp `json:"endTime"` - - // StartTime: Optional. The beginning of the time interval. The default - // value for the start time is the end time. The start time must not be - // later than the end time. - StartTime *Timestamp `json:"startTime,omitempty"` -} - -type DynamicMonitoredResource struct { - // The unique name of the resource - Name string `json:"name,required"` - // Type: Required. The resource type of the resource - // General Nagios Types are hosts, whereas CloudHub can have richer complexity - Type ResourceType `json:"type,required"` - // Owner relationship for associations like hypervisor->virtual machine - Owner string `json:"owner,omitempty"` - // CloudHub Categorization of resources - Category string `json:"category,omitempty"` - // Optional description of this resource, such as Nagios notes - Description string `json:"description,omitempty"` - // Foundation Properties - Properties map[string]TypedValue `json:"properties,omitempty"` - // Device (usually IP address), leave empty if not available, will default to name - Device string `json:"device,omitempty"` - // Restrict to a Groundwork Monitor Status - Status MonitorStatus `json:"status,required"` - // The last status check time on this resource - LastCheckTime *Timestamp `json:"lastCheckTime,omitempty"` - // The next status check time on this resource - NextCheckTime *Timestamp `json:"nextCheckTime,omitempty"` - // Nagios plugin output string - LastPlugInOutput string `json:"lastPluginOutput,omitempty"` - // Services state collection - Services []DynamicMonitoredService `json:"services"` -} - -// A DynamicMonitoredService represents a Groundwork Service creating during a metrics scan. -// In cloud systems, services are usually modeled as a complex metric definition, with each sampled -// metric variation represented as as single metric time series. -// -// A DynamicMonitoredService contains a collection of TimeSeries Metrics. -// MonitoredService collections are attached to a DynamicMonitoredResource during a metrics scan. -type DynamicMonitoredService struct { - // The unique name of the resource - Name string `json:"name,required"` - // Type: Required. The resource type of the resource - // General Nagios Types are hosts, whereas CloudHub can have richer complexity - Type ResourceType `json:"type,required"` - // Owner relationship for associations like hypervisor->virtual machine - Owner string `json:"owner,omitempty"` - // CloudHub Categorization of resources - Category string `json:"category,omitempty"` - // Optional description of this resource, such as Nagios notes - Description string `json:"description,omitempty"` - // Foundation Properties - Properties map[string]TypedValue `json:"properties,omitempty"` - // Restrict to a Groundwork Monitor Status - Status MonitorStatus `json:"status,required"` - // The last status check time on this resource - LastCheckTime *Timestamp `json:"lastCheckTime,omitempty"` - // Nagios plugin output string - LastPlugInOutput string `json:"lastPluginOutput,omitempty"` - // metrics - Metrics []TimeSeries `json:"metrics"` -} - -// ThresholdValue describes threshold -type ThresholdValue struct { - SampleType MetricSampleType `json:"sampleType"` - Label string `json:"label"` - Value *TypedValue `json:"value"` -} - -// TimeSeries defines a single Metric Sample, its time interval, and 0 or more thresholds -type TimeSeries struct { - MetricName string `json:"metricName"` - SampleType MetricSampleType `json:"sampleType,omitEmpty"` - // Interval: The time interval to which the data sample applies. For - // GAUGE metrics, only the end time of the interval is used. For DELTA - // metrics, the start and end time should specify a non-zero interval, - // with subsequent samples specifying contiguous and non-overlapping - // intervals. For CUMULATIVE metrics, the start and end time should - // specify a non-zero interval, with subsequent samples specifying the - // same start time and increasing end times, until an event resets the - // cumulative value to zero and sets a new start time for the following - // samples. - Interval *TimeInterval `json:"interval"` - Value *TypedValue `json:"value"` - Tags map[string]string `json:"tags,omitempty"` - Unit UnitType `json:"unit,omitempty"` - Thresholds *[]ThresholdValue `json:"thresholds,omitempty"` -} - -// DynamicResourcesWithServicesRequest defines SendResourcesWithMetrics payload -type DynamicResourcesWithServicesRequest struct { - Context *TracerContext `json:"context,omitempty"` - Resources []DynamicMonitoredResource `json:"resources"` - Groups []ResourceGroup `json:"groups,omitempty"` -} - -// ResourceGroup defines group entity -type ResourceGroup struct { - GroupName string `json:"groupName,required"` - Type GroupType `json:"type,required"` - Description string `json:"description,omitempty"` - Resources []MonitoredResourceRef `json:"resources,required"` -} - -// GroupType defines the foundation group type -type GroupType string - -// The group type uniquely defining corresponding foundation group type -const ( - HostGroup GroupType = "HostGroup" - ServiceGroup = "ServiceGroup" - CustomGroup = "CustomGroup" -) - -// MonitoredResourceRef references a MonitoredResource in a group collection -type MonitoredResourceRef struct { - // The unique name of the resource - Name string `json:"name,required"` - // Type: Optional. The resource type uniquely defining the resource type - // General Nagios Types are host and service, whereas CloudHub can have richer complexity - Type ResourceType `json:"type,omitempty"` - // Owner relationship for associations like host->service - Owner string `json:"owner,omitempty"` -} - -// TracerContext describes a Transit call -type TracerContext struct { - AppType string `json:"appType"` - AgentID string `json:"agentId"` - TraceToken string `json:"traceToken"` - TimeStamp *Timestamp `json:"timeStamp"` - Version VersionString `json:"version"` -} - -// VersionString defines type of constant -type VersionString string - -// ModelVersion defines versioning -const ( - ModelVersion VersionString = "1.0.0" -) diff --git a/plugins/outputs/gw8/gw8.go b/plugins/outputs/gw8/gw8.go deleted file mode 100644 index 82536a6e2b254..0000000000000 --- a/plugins/outputs/gw8/gw8.go +++ /dev/null @@ -1,447 +0,0 @@ -package gw8 - -import ( - "encoding/binary" - "encoding/json" - "errors" - "fmt" - "github.com/hashicorp/go-uuid" - "github.com/influxdata/telegraf" - "github.com/influxdata/telegraf/internal" - "github.com/influxdata/telegraf/plugins/outputs" - "github.com/influxdata/telegraf/plugins/outputs/gw8/addons" - "github.com/patrickmn/go-cache" - "net/http" - "strconv" - "sync" - "time" -) - -const ( - defaultMonitoringRoute = "/api/monitoring?dynamic=true" -) - -// Login and logout routes from Groundwork API -const ( - loginURL = "/api/auth/login" - logoutURL = "/api/auth/logout" -) - -var ( - tracerOnce sync.Once -) - -// Variables for building and updating tracer token -var ( - tracerToken []byte - cacheKeyTracerToken = "cacheKeyTraceToken" - tracerCache = cache.New(-1, -1) -) - -var ( - sampleConfig = ` - ## HTTP endpoint for your groundwork instance. - endpoint = "" - - ## Agent uuid for Groundwork API Server - agent_id = "" - - ## Groundwork application type - app_type = "" - - ## Username to access Groundwork API - username = "" - - ## Password to use in pair with username - password = "" - - ## Default display name for the host with services(metrics) - default_host = "default_telegraf" - - ## Default service state [default - "SERVICE_OK"] - default_service_state = "SERVICE_OK" - - ## The name of the tag that contains the hostname [default - "host"] - resource_tag = "host" -` -) - -type GW8 struct { - Server string `toml:"groundwork_endpoint"` - AgentID string `toml:"agent_id"` - AppType string `toml:"app_type"` - Username string `toml:"username"` - Password string `toml:"password"` - DefaultHost string `toml:"default_host"` - DefaultServiceState string `toml:"default_service_state"` - ResourceTag string `toml:"resource_tag"` - authToken string -} - -func (g *GW8) SampleConfig() string { - return sampleConfig -} - -func (g *GW8) Connect() error { - if g.Server == "" { - return errors.New("no 'server' provided") - } - - byteToken, err := login(g.Server+loginURL, g.Username, g.Password) - if err == nil { - g.authToken = string(byteToken) - } - - return err -} - -func (g *GW8) Close() error { - formValues := map[string]string{ - "gwos-app-name": "gw8", - "gwos-api-token": g.authToken, - } - - headers := map[string]string{ - "Content-Type": "application/x-www-form-urlencoded", - } - - _, _, err := addons.SendRequest(http.MethodPost, g.Server+logoutURL, headers, formValues, nil) - - return err -} - -func (g *GW8) Write(metrics []telegraf.Metric) error { - resourceToServicesMap := make(map[string][]addons.DynamicMonitoredService) - for _, metric := range metrics { - resource, service := parseMetric(g.DefaultHost, g.DefaultServiceState, g.ResourceTag, metric) - resourceToServicesMap[resource] = append(resourceToServicesMap[resource], service) - } - - var resources []addons.DynamicMonitoredResource - for resourceName, services := range resourceToServicesMap { - resources = append(resources, addons.DynamicMonitoredResource{ - Name: resourceName, - Type: addons.Host, - Status: addons.HostUp, - LastCheckTime: addons.Now(), - Services: services, - }) - } - - requestJSON, err := json.Marshal(addons.DynamicResourcesWithServicesRequest{ - Context: &addons.TracerContext{ - AppType: g.AppType, - AgentID: g.AgentID, - TraceToken: makeTracerToken(), - TimeStamp: addons.Now(), - Version: addons.ModelVersion, - }, - Resources: resources, - Groups: nil, - }) - - if err != nil { - return err - } - - headers := map[string]string{ - "GWOS-APP-NAME": "gw8", - "GWOS-API-TOKEN": g.authToken, - "Content-Type": "application/json", - "Accept": "application/json", - } - - statusCode, _, err := addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) - if err != nil { - return err - } - - /* Re-login mechanism */ - if statusCode == 401 { - if err = g.Connect(); err != nil { - return err - } - headers["GWOS-API-TOKEN"] = g.authToken - statusCode, body, httpErr := addons.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) - if httpErr != nil { - return httpErr - } - if statusCode != 200 { - return fmt.Errorf("something went wrong during processing an http request[http_status = %d, body = %s]", statusCode, string(body)) - } - } - - return nil -} - -func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metric telegraf.Metric) (string, addons.DynamicMonitoredService) { - resource := "default_telegraf" - if defaultHostname != "" { - resource = defaultHostname - } - if resourceTag == "" { - resourceTag = "host" - } - if value, present := metric.GetTag(resourceTag); present { - resource = value - } - - service := metric.Name() - if value, present := metric.GetTag("service"); present { - service = value - } - - status := string(addons.ServiceOk) - if defaultServiceState != "" && validStatus(defaultServiceState) { - status = defaultServiceState - } - if value, present := metric.GetTag("status"); present { - if validStatus(value) { - status = value - } - } - - message := "" - if value, present := metric.GetTag("message"); present { - message = value - } - - unitType := string(addons.UnitCounter) - if value, present := metric.GetTag("unitType"); present { - unitType = value - } - - critical := -1.0 - if value, present := metric.GetTag("critical"); present { - if s, err := strconv.ParseFloat(value, 64); err == nil { - critical = s - } - } - - warning := -1.0 - if value, present := metric.GetTag("warning"); present { - if s, err := strconv.ParseFloat(value, 64); err == nil { - warning = s - } - } - - serviceObject := addons.DynamicMonitoredService{ - Name: service, - Type: addons.Service, - Owner: resource, - Status: addons.MonitorStatus(status), - LastCheckTime: addons.Now(), - LastPlugInOutput: message, - Metrics: nil, - } - - for _, value := range metric.FieldList() { - var thresholds []addons.ThresholdValue - thresholds = append(thresholds, addons.ThresholdValue{ - SampleType: addons.Warning, - Label: value.Key + "_wn", - Value: &addons.TypedValue{ - ValueType: addons.DoubleType, - DoubleValue: warning, - }, - }) - thresholds = append(thresholds, addons.ThresholdValue{ - SampleType: addons.Critical, - Label: value.Key + "_cr", - Value: &addons.TypedValue{ - ValueType: addons.DoubleType, - DoubleValue: critical, - }, - }) - - valueType := addons.DoubleType - var floatVal float64 - var stringVal *string - - switch value.Value.(type) { - case string: - valueType = addons.StringType - tmpStr := value.Value.(string) - stringVal = &tmpStr - default: - floatVal, _ = internal.ToFloat64(value.Value) - } - serviceObject.Metrics = append(serviceObject.Metrics, addons.TimeSeries{ - MetricName: value.Key, - SampleType: addons.Value, - Interval: &addons.TimeInterval{ - EndTime: addons.TimestampRef(metric.Time()), - }, - Value: &addons.TypedValue{ - ValueType: valueType, - DoubleValue: floatVal, - StringValue: stringVal, - }, - Unit: addons.UnitType(unitType), - Thresholds: &thresholds, - }) - } - - serviceObject.Status, _ = calculateServiceStatus(&serviceObject.Metrics) - - return resource, serviceObject -} - -func (g *GW8) Description() string { - return "Send telegraf metrics to groundwork" -} - -func login(url, username, password string) ([]byte, error) { - formValues := map[string]string{ - "user": username, - "password": password, - "gwos-app-name": "gw8", - } - headers := map[string]string{ - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "text/plain", - } - - statusCode, body, err := addons.SendRequest(http.MethodPost, url, headers, formValues, nil) - if err != nil { - return nil, err - } - if statusCode != 200 { - return nil, fmt.Errorf("[ERROR]: Http request failed. [Status code]: %d, [Response]: %s", statusCode, string(body)) - } - - return body, nil -} - -func calculateServiceStatus(metrics *[]addons.TimeSeries) (addons.MonitorStatus, error) { - if metrics == nil || len(*metrics) == 0 { - return addons.ServiceUnknown, nil - } - previousStatus := addons.ServiceOk - for _, metric := range *metrics { - if metric.Thresholds != nil { - var warning, critical addons.ThresholdValue - for _, threshold := range *metric.Thresholds { - switch threshold.SampleType { - case addons.Warning: - warning = threshold - case addons.Critical: - critical = threshold - default: - return addons.ServiceOk, fmt.Errorf("unsupported threshold Sample type") - } - } - - status := calculateStatus(metric.Value, warning.Value, critical.Value) - if addons.MonitorStatusWeightService[status] > addons.MonitorStatusWeightService[previousStatus] { - previousStatus = status - } - } - } - return previousStatus, nil -} - -func calculateStatus(value *addons.TypedValue, warning *addons.TypedValue, critical *addons.TypedValue) addons.MonitorStatus { - if warning == nil && critical == nil { - return addons.ServiceOk - } - - var warningValue float64 - var criticalValue float64 - - if warning != nil { - if warning.ValueType == addons.DoubleType { - warningValue = warning.DoubleValue - } - } - - if critical != nil { - if critical.ValueType == addons.DoubleType { - criticalValue = critical.DoubleValue - } - } - - if value.ValueType == addons.DoubleType { - if warning == nil && criticalValue == -1 { - if value.DoubleValue >= criticalValue { - return addons.ServiceUnscheduledCritical - } - return addons.ServiceOk - } - if critical == nil && (warning != nil && warningValue == -1) { - if value.DoubleValue >= warningValue { - return addons.ServiceWarning - } - return addons.ServiceOk - } - if (warning != nil && critical != nil) && (warningValue == -1 || criticalValue == -1) { - return addons.ServiceOk - } - // is it a reverse comparison (low to high) - if warningValue > criticalValue { - if value.DoubleValue <= criticalValue { - return addons.ServiceUnscheduledCritical - } - if value.DoubleValue <= warningValue { - return addons.ServiceWarning - } - return addons.ServiceOk - } - if value.DoubleValue >= criticalValue { - return addons.ServiceUnscheduledCritical - } - if value.DoubleValue >= warningValue { - return addons.ServiceWarning - } - - return addons.ServiceOk - } - return addons.ServiceOk -} - -func validStatus(status string) bool { - switch addons.MonitorStatus(status) { - case addons.ServiceOk, addons.ServiceWarning, addons.ServicePending, addons.ServiceScheduledCritical, - addons.ServiceUnscheduledCritical, addons.ServiceUnknown: - return true - } - return false -} - -// makeTracerContext -func makeTracerToken() string { - tracerOnce.Do(initTracerToken) - - /* combine TraceToken from fixed and incremental parts */ - tokenBuf := make([]byte, 16) - copy(tokenBuf, tracerToken) - if tokenInc, err := tracerCache.IncrementUint64(cacheKeyTracerToken, 1); err == nil { - binary.PutUvarint(tokenBuf, tokenInc) - } else { - /* fallback with timestamp */ - binary.PutVarint(tokenBuf, time.Now().UnixNano()) - } - traceToken, _ := uuid.FormatUUID(tokenBuf) - - return traceToken -} - -func initTracerToken() { - /* prepare random tracerToken */ - token := []byte("aaaabbbbccccdddd") - if randBuf, err := uuid.GenerateRandomBytes(16); err == nil { - copy(tracerToken, randBuf) - } else { - /* fallback with multiplied timestamp */ - binary.PutVarint(tracerToken, time.Now().UnixNano()) - binary.PutVarint(tracerToken[6:], time.Now().UnixNano()) - } - tracerCache.Set(cacheKeyTracerToken, uint64(1), -1) - tracerToken = token -} - -func init() { - outputs.Add("gw8", func() telegraf.Output { - return &GW8{} - }) -} diff --git a/plugins/outputs/gw8/gw8_test.go b/plugins/outputs/gw8/gw8_test.go deleted file mode 100644 index 39d1d295a7405..0000000000000 --- a/plugins/outputs/gw8/gw8_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package gw8 - -import ( - "encoding/json" - "fmt" - "github.com/influxdata/telegraf" - "github.com/influxdata/telegraf/testutil" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" -) - -const ( - defaultTestAgentID = "ec1676cc-583d-48ee-b035-7fb5ed0fcf88" - defaultTestAppType = "TELEGRAF" -) - -func TestWrite(t *testing.T) { - // Generate test metric with default name to test Write logic - floatMetric := testutil.TestMetric(1, "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) - assert.NoError(t, err) - - // Decode body to use in assertations below - var obj GroundworkObject - err = json.Unmarshal(body, &obj) - assert.NoError(t, err) - - // Check if server gets valid metrics object - assert.Equal(t, obj["context"].(map[string]interface{})["appType"], defaultTestAppType) - assert.Equal(t, obj["context"].(map[string]interface{})["agentId"], defaultTestAgentID) - assert.Equal(t, obj["resources"].([]interface{})[0].(map[string]interface{})["name"], "default_telegraf") - assert.Equal( - t, - obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[0].(map[string]interface{})["name"], - "Float", - ) - assert.Equal( - t, - obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[0].(map[string]interface{})["metrics"].([]interface{})[0].(map[string]interface{})["value"].(map[string]interface{})["doubleValue"].(float64), - 1.0, - ) - assert.Equal( - t, - obj["resources"].([]interface{})[0].(map[string]interface{})["services"].([]interface{})[1].(map[string]interface{})["metrics"].([]interface{})[0].(map[string]interface{})["value"].(map[string]interface{})["stringValue"].(string), - "Test", - ) - - _, err = fmt.Fprintln(w, `OK`) - assert.NoError(t, err) - })) - - i := GW8{ - Server: server.URL, - AppType: defaultTestAppType, - AgentID: defaultTestAgentID, - } - - err := i.Write([]telegraf.Metric{floatMetric, stringMetric}) - assert.NoError(t, err) - - defer server.Close() -} - -type GroundworkObject map[string]interface{} From 76933d1cd1ab4ea5a8d466aa86e85684a0608ee7 Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Tue, 23 Nov 2021 17:57:05 +0300 Subject: [PATCH 05/11] feat: provide minor fixes --- go.mod | 4 +- go.sum | 2 - plugins/outputs/groundwork/README.md | 3 +- plugins/outputs/groundwork/groundwork.go | 83 ++++++++++++++---------- 4 files changed, 49 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index aae3a3e8fb7af..1f9ae3a05eb97 100644 --- a/go.mod +++ b/go.mod @@ -327,12 +327,11 @@ require ( modernc.org/token v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect sigs.k8s.io/yaml v1.2.0 // indirect + github.com/gwos/tcg/transit v0.0.0-20211124103916-e2b3b806f031 ) require github.com/libp2p/go-reuseport v0.1.0 -require github.com/gwos/tcg v0.0.0-20211123085413-fa76511f6546 - require ( github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.2.0 // indirect github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.4.0 // indirect @@ -354,7 +353,6 @@ require ( go.opentelemetry.io/otel/sdk/export/metric v0.24.0 // indirect go.opentelemetry.io/otel/trace v1.0.1 // indirect go.opentelemetry.io/proto/otlp v0.9.0 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible ) // replaced due to https://github.com/satori/go.uuid/issues/73 diff --git a/go.sum b/go.sum index df31dc99552be..72cea899ec60d 100644 --- a/go.sum +++ b/go.sum @@ -1710,8 +1710,6 @@ github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIw github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= github.com/pavius/impi v0.0.3/go.mod h1:x/hU0bfdWIhuOT1SKwiJg++yvkk6EuOtJk8WtDZqgr8= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= diff --git a/plugins/outputs/groundwork/README.md b/plugins/outputs/groundwork/README.md index 3e54b40697d44..308073e50d885 100644 --- a/plugins/outputs/groundwork/README.md +++ b/plugins/outputs/groundwork/README.md @@ -1,6 +1,6 @@ # Groundwork Output Plugin -This plugin writes to a [GroundWork Monitor][1] instance. +This plugin writes to a [GroundWork Monitor][1] instance. Plugin only supports GW8+ [1]: https://www.gwos.com/product/groundwork-monitor/ @@ -29,4 +29,3 @@ This plugin writes to a [GroundWork Monitor][1] instance. ## The name of the tag that contains the hostname [default - "host"] resource_tag = "host" ``` -#### NOTE: Plugin only supports GW8+ \ No newline at end of file diff --git a/plugins/outputs/groundwork/groundwork.go b/plugins/outputs/groundwork/groundwork.go index 3205d39ae79ee..82d17635586e1 100644 --- a/plugins/outputs/groundwork/groundwork.go +++ b/plugins/outputs/groundwork/groundwork.go @@ -4,9 +4,8 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gwos/tcg/clients" - "github.com/gwos/tcg/milliseconds" "github.com/gwos/tcg/transit" + "github.com/gwos/tcg/transit/clients" "github.com/hashicorp/go-uuid" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/internal" @@ -34,9 +33,6 @@ var ( ## Agent uuid for Groundwork API Server agent_id = "" - ## Groundwork application type - app_type = "" - ## Username to access Groundwork API username = "" @@ -73,6 +69,9 @@ func (g *Groundwork) Init() error { if g.Server == "" { return errors.New("no 'groundwork_endpoint' provided") } + if g.AgentID == "" { + return errors.New("no 'agent_id' provided") + } if g.Username == "" { return errors.New("no 'username' provided") } @@ -80,13 +79,16 @@ func (g *Groundwork) Init() error { return errors.New("no 'password' provided") } if g.DefaultHost == "" { - g.DefaultHost = "telegraf" + return errors.New("no 'default_host' provided") + } + if g.DefaultServiceState == "" { + return errors.New("no 'default_service_state' provided") } if g.ResourceTag == "" { - g.ResourceTag = "host" + return errors.New("no 'resource_tag' provided") } - if g.DefaultServiceState == "" || !validStatus(g.DefaultServiceState) { - g.DefaultServiceState = string(transit.ServiceOk) + if !validStatus(g.DefaultServiceState) { + return errors.New("invalid 'default_service_state' provided") } return nil @@ -109,6 +111,7 @@ func (g *Groundwork) Close() error { headers := map[string]string{ "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": internal.ProductToken(), } _, _, err := clients.SendRequest(http.MethodPost, g.Server+logoutURL, headers, formValues, nil) @@ -119,7 +122,7 @@ func (g *Groundwork) Close() error { func (g *Groundwork) Write(metrics []telegraf.Metric) error { resourceToServicesMap := make(map[string][]transit.DynamicMonitoredService) for _, metric := range metrics { - resource, service := parseMetric(g.DefaultHost, g.DefaultServiceState, g.ResourceTag, metric) + resource, service := g.parseMetric(metric) resourceToServicesMap[resource] = append(resourceToServicesMap[resource], service) } @@ -133,8 +136,7 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { }, }, Status: transit.HostUp, - LastCheckTime: milliseconds.MillisecondTimestamp{Time: time.Now()}, - NextCheckTime: milliseconds.MillisecondTimestamp{Time: time.Now()}, + LastCheckTime: transit.MillisecondTimestamp{Time: time.Now()}, Services: services, }) } @@ -145,7 +147,7 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { AppType: "TELEGRAF", AgentID: g.AgentID, TraceToken: traceToken, - TimeStamp: milliseconds.MillisecondTimestamp{Time: time.Now()}, + TimeStamp: transit.MillisecondTimestamp{Time: time.Now()}, Version: transit.ModelVersion, }, Resources: resources, @@ -157,13 +159,14 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { } headers := map[string]string{ - "GWOS-APP-NAME": "groundwork", + "GWOS-APP-NAME": "telegraf", "GWOS-API-TOKEN": g.authToken, "Content-Type": "application/json", "Accept": "application/json", + "User-Agent": internal.ProductToken(), } - statusCode, _, err := clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) + statusCode, body, err := clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) if err != nil { return err } @@ -174,15 +177,16 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { return err } headers["GWOS-API-TOKEN"] = g.authToken - statusCode, body, httpErr := clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) - if httpErr != nil { - return httpErr - } - if statusCode != 200 { - return fmt.Errorf("something went wrong during processing an http request[http_status = %d, body = %s]", statusCode, string(body)) + statusCode, body, err = clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) + if err != nil { + return err } } + if statusCode != 200 { + return fmt.Errorf("something went wrong during processing an http request[http_status = %d, body = %s]", statusCode, string(body)) + } + return nil } @@ -192,14 +196,17 @@ func (g *Groundwork) Description() string { func init() { outputs.Add("groundwork", func() telegraf.Output { - return &Groundwork{} + return &Groundwork{ + ResourceTag: "host", + DefaultHost: "telegraf", + DefaultServiceState: string(transit.ServiceOk), + } }) } -func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metric telegraf.Metric) (string, transit.DynamicMonitoredService) { - resource := defaultHostname - - if value, present := metric.GetTag(resourceTag); present { +func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.DynamicMonitoredService) { + resource := g.DefaultHost + if value, present := metric.GetTag(g.ResourceTag); present { resource = value } @@ -208,11 +215,10 @@ func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metri service = value } - status := defaultServiceState - if value, present := metric.GetTag("status"); present { - if validStatus(value) { - status = value - } + status := g.DefaultServiceState + value, statusPresent := metric.GetTag("status") + if validStatus(value) { + status = value } message := "" @@ -246,8 +252,7 @@ func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metri Owner: resource, }, Status: transit.MonitorStatus(status), - LastCheckTime: milliseconds.MillisecondTimestamp{Time: time.Now()}, - NextCheckTime: milliseconds.MillisecondTimestamp{}, + LastCheckTime: transit.MillisecondTimestamp{Time: metric.Time()}, LastPlugInOutput: message, Metrics: nil, } @@ -287,7 +292,7 @@ func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metri MetricName: value.Key, SampleType: transit.Value, Interval: &transit.TimeInterval{ - EndTime: milliseconds.MillisecondTimestamp{Time: metric.Time()}, + EndTime: transit.MillisecondTimestamp{Time: metric.Time()}, }, Value: &transit.TypedValue{ ValueType: valueType, @@ -299,7 +304,12 @@ func parseMetric(defaultHostname, defaultServiceState, resourceTag string, metri }) } - serviceObject.Status, _ = transit.CalculateServiceStatus(&serviceObject.Metrics) + if !statusPresent { + var err error + if serviceObject.Status, err = transit.CalculateServiceStatus(&serviceObject.Metrics); err != nil { + serviceObject.Status = transit.MonitorStatus(g.DefaultServiceState) + } + } return resource, serviceObject } @@ -308,11 +318,12 @@ func login(url, username, password string) ([]byte, error) { formValues := map[string]string{ "user": username, "password": password, - "gwos-app-name": "groundwork", + "gwos-app-name": "telegraf", } headers := map[string]string{ "Content-Type": "application/x-www-form-urlencoded", "Accept": "text/plain", + "User-Agent": internal.ProductToken(), } statusCode, body, err := clients.SendRequest(http.MethodPost, url, headers, formValues, nil) From 431862e57e56cb93c799338e2fae5cc6ce9fa6a9 Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Wed, 24 Nov 2021 15:30:59 +0300 Subject: [PATCH 06/11] feat: update README.md --- plugins/outputs/groundwork/README.md | 27 ++++++++++++++++++------ plugins/outputs/groundwork/groundwork.go | 14 ++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/plugins/outputs/groundwork/README.md b/plugins/outputs/groundwork/README.md index 308073e50d885..6dd17f047551c 100644 --- a/plugins/outputs/groundwork/README.md +++ b/plugins/outputs/groundwork/README.md @@ -9,23 +9,36 @@ This plugin writes to a [GroundWork Monitor][1] instance. Plugin only supports G ```toml [[outputs.groundwork]] ## HTTP endpoint for your groundwork instance. - groundwork_endpoint = "" + # groundwork_endpoint = "" ## Agent uuid for Groundwork API Server - agent_id = "" + # agent_id = "" ## Username to access Groundwork API - username = "" + # username = "" ## Password to use in pair with username - password = "" + # password = "" ## Default display name for the host with services(metrics) [default - "telegraf"] - default_host = "telegraf" + # default_host = "telegraf" ## Default service state [default - "SERVICE_OK"] - default_service_state = "SERVICE_OK" + # default_service_state = "SERVICE_OK" ## The name of the tag that contains the hostname [default - "host"] - resource_tag = "host" + # 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) + • warning - to define warning threshold value + • critical - to define critical threshold value +``` + + diff --git a/plugins/outputs/groundwork/groundwork.go b/plugins/outputs/groundwork/groundwork.go index 82d17635586e1..d3848e6cac570 100644 --- a/plugins/outputs/groundwork/groundwork.go +++ b/plugins/outputs/groundwork/groundwork.go @@ -28,25 +28,25 @@ const ( var ( sampleConfig = ` ## HTTP endpoint for your groundwork instance. - endpoint = "" + # endpoint = "" ## Agent uuid for Groundwork API Server - agent_id = "" + # agent_id = "" ## Username to access Groundwork API - username = "" + # username = "" ## Password to use in pair with username - password = "" + # password = "" ## Default display name for the host with services(metrics) - default_host = "default_telegraf" + # default_host = "telegraf" ## Default service state [default - "SERVICE_OK"] - default_service_state = "SERVICE_OK" + # default_service_state = "SERVICE_OK" ## The name of the tag that contains the hostname [default - "host"] - resource_tag = "host" + # resource_tag = "host" ` ) From 8b1afb84d121f06f880838a50c56e73213ca8359 Mon Sep 17 00:00:00 2001 From: Pavlo Sumkin Date: Wed, 24 Nov 2021 17:39:14 +0200 Subject: [PATCH 07/11] feat: update go mod --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 1f9ae3a05eb97..093287346dfaa 100644 --- a/go.mod +++ b/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/transit v0.0.0-20211124103916-e2b3b806f031 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 @@ -327,7 +328,6 @@ require ( modernc.org/token v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect sigs.k8s.io/yaml v1.2.0 // indirect - github.com/gwos/tcg/transit v0.0.0-20211124103916-e2b3b806f031 ) require github.com/libp2p/go-reuseport v0.1.0 diff --git a/go.sum b/go.sum index 72cea899ec60d..005330825832b 100644 --- a/go.sum +++ b/go.sum @@ -1145,8 +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 v0.0.0-20211123085413-fa76511f6546 h1:qq2iGm+3HvHr2w3asvxxSxDxjmwBEVJ2vy1zCLp0ZOo= -github.com/gwos/tcg v0.0.0-20211123085413-fa76511f6546/go.mod h1:C7Y3c2iI8mc+VKHaNum0dzP0iWnljEnp8bZ9hrBLK18= +github.com/gwos/tcg/transit v0.0.0-20211124103916-e2b3b806f031 h1:FLKSa4N5aS/Bvj7YcuAZpfjtlfMF/e9/agL8pW0rtgc= +github.com/gwos/tcg/transit v0.0.0-20211124103916-e2b3b806f031/go.mod h1:1AyzNx6r9l3khJ/ae4cJyn4NLlAZMy1l3K5QIIm2nBc= 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= From e9050e397cdd041d3a2061c41ca1b3dfd35ea25a Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Thu, 25 Nov 2021 15:29:37 +0300 Subject: [PATCH 08/11] feat: improve logging and add license --- docs/LICENSE_OF_DEPENDENCIES.md | 1 + go.mod | 2 +- go.sum | 4 +- plugins/outputs/groundwork/README.md | 44 ++++---- plugins/outputs/groundwork/groundwork.go | 128 +++++++++++++---------- 5 files changed, 95 insertions(+), 84 deletions(-) diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index 440349a6f5c91..5904b938f19d3 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -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 [TCG 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) diff --git a/go.mod b/go.mod index 093287346dfaa..b2d7530d8663a 100644 --- a/go.mod +++ b/go.mod @@ -133,7 +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/transit v0.0.0-20211124103916-e2b3b806f031 + github.com/gwos/tcg/sdk v0.0.0-20211125122009-24221bf01623 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 diff --git a/go.sum b/go.sum index 005330825832b..667e100fb6c9b 100644 --- a/go.sum +++ b/go.sum @@ -1145,8 +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/transit v0.0.0-20211124103916-e2b3b806f031 h1:FLKSa4N5aS/Bvj7YcuAZpfjtlfMF/e9/agL8pW0rtgc= -github.com/gwos/tcg/transit v0.0.0-20211124103916-e2b3b806f031/go.mod h1:1AyzNx6r9l3khJ/ae4cJyn4NLlAZMy1l3K5QIIm2nBc= +github.com/gwos/tcg/sdk v0.0.0-20211125122009-24221bf01623 h1:WiBrZ6coNJJhqJeaUpWT4jVSMJoIjKa+wumRUhLQy9Y= +github.com/gwos/tcg/sdk v0.0.0-20211125122009-24221bf01623/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= diff --git a/plugins/outputs/groundwork/README.md b/plugins/outputs/groundwork/README.md index 6dd17f047551c..235753ed53be7 100644 --- a/plugins/outputs/groundwork/README.md +++ b/plugins/outputs/groundwork/README.md @@ -1,44 +1,38 @@ -# Groundwork Output Plugin +# 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: +### Configuration ```toml [[outputs.groundwork]] - ## HTTP endpoint for your groundwork instance. - # groundwork_endpoint = "" + ## URL of your groundwork instance. + url = "https://groundwork.example.com" - ## Agent uuid for Groundwork API Server - # agent_id = "" + ## Agent uuid for GroundWork API Server. + agent_id = "" - ## Username to access Groundwork API - # username = "" + ## Username and password to access GroundWork API. + username = "" + password = "" - ## Password to use in pair with username - # password = "" - - ## Default display name for the host with services(metrics) [default - "telegraf"] + ## Default display name for the host with services(metrics). # default_host = "telegraf" - ## Default service state [default - "SERVICE_OK"] + ## Default service state. # default_service_state = "SERVICE_OK" - ## The name of the tag that contains the hostname [default - "host"] + ## 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) - • warning - to define warning threshold value - • critical - to define critical threshold value -``` - +### 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. diff --git a/plugins/outputs/groundwork/groundwork.go b/plugins/outputs/groundwork/groundwork.go index d3848e6cac570..640c92ff40ae7 100644 --- a/plugins/outputs/groundwork/groundwork.go +++ b/plugins/outputs/groundwork/groundwork.go @@ -4,15 +4,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gwos/tcg/transit" - "github.com/gwos/tcg/transit/clients" + "net/http" + "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/internal" "github.com/influxdata/telegraf/plugins/outputs" - "net/http" - "strconv" - "time" ) const ( @@ -27,31 +27,29 @@ const ( var ( sampleConfig = ` - ## HTTP endpoint for your groundwork instance. - # endpoint = "" - - ## Agent uuid for Groundwork API Server - # agent_id = "" + ## URL of your groundwork instance. + url = "https://groundwork.example.com" - ## Username to access Groundwork API - # username = "" + ## Agent uuid for GroundWork API Server. + agent_id = "" - ## Password to use in pair with username - # password = "" + ## Username and password to access GroundWork API. + username = "" + password = "" - ## Default display name for the host with services(metrics) + ## Default display name for the host with services(metrics). # default_host = "telegraf" - ## Default service state [default - "SERVICE_OK"] + ## Default service state. # default_service_state = "SERVICE_OK" - ## The name of the tag that contains the hostname [default - "host"] + ## The name of the tag that contains the hostname. # resource_tag = "host" ` ) type Groundwork struct { - Server string `toml:"groundwork_endpoint"` + Server string `toml:"url"` AgentID string `toml:"agent_id"` Username string `toml:"username"` Password string `toml:"password"` @@ -59,6 +57,8 @@ type Groundwork struct { DefaultServiceState string `toml:"default_service_state"` ResourceTag string `toml:"resource_tag"` authToken string + + Log telegraf.Logger `toml:"-"` } func (g *Groundwork) SampleConfig() string { @@ -67,7 +67,7 @@ func (g *Groundwork) SampleConfig() string { func (g *Groundwork) Init() error { if g.Server == "" { - return errors.New("no 'groundwork_endpoint' provided") + return errors.New("no 'url' provided") } if g.AgentID == "" { return errors.New("no 'agent_id' provided") @@ -81,9 +81,6 @@ func (g *Groundwork) Init() error { if g.DefaultHost == "" { return errors.New("no 'default_host' provided") } - if g.DefaultServiceState == "" { - return errors.New("no 'default_service_state' provided") - } if g.ResourceTag == "" { return errors.New("no 'resource_tag' provided") } @@ -96,11 +93,13 @@ func (g *Groundwork) Init() error { func (g *Groundwork) Connect() error { byteToken, err := login(g.Server+loginURL, g.Username, g.Password) - if err == nil { - g.authToken = string(byteToken) + if err != nil { + return fmt.Errorf("could not log in at %s: %v", g.Server+loginURL, err) } - return err + g.authToken = string(byteToken) + + return nil } func (g *Groundwork) Close() error { @@ -114,9 +113,11 @@ func (g *Groundwork) Close() error { "User-Agent": internal.ProductToken(), } - _, _, err := clients.SendRequest(http.MethodPost, g.Server+logoutURL, headers, formValues, nil) + if _, _, err := clients.SendRequest(http.MethodPost, g.Server+logoutURL, headers, formValues, nil); err != nil { + return fmt.Errorf("could not log out at %s: %v", g.Server+logoutURL, err) + } - return err + return nil } func (g *Groundwork) Write(metrics []telegraf.Metric) error { @@ -136,7 +137,7 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { }, }, Status: transit.HostUp, - LastCheckTime: transit.MillisecondTimestamp{Time: time.Now()}, + LastCheckTime: transit.NewTimestamp(), Services: services, }) } @@ -147,7 +148,7 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { AppType: "TELEGRAF", AgentID: g.AgentID, TraceToken: traceToken, - TimeStamp: transit.MillisecondTimestamp{Time: time.Now()}, + TimeStamp: transit.NewTimestamp(), Version: transit.ModelVersion, }, Resources: resources, @@ -168,23 +169,23 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { statusCode, body, err := clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) if err != nil { - return err + return fmt.Errorf("error while sending: %v", err) } /* Re-login mechanism */ if statusCode == 401 { if err = g.Connect(); err != nil { - return err + return fmt.Errorf("re-login failed: %v", err) } headers["GWOS-API-TOKEN"] = g.authToken statusCode, body, err = clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) if err != nil { - return err + return fmt.Errorf("error while sending: %v", err) } } if statusCode != 200 { - return fmt.Errorf("something went wrong during processing an http request[http_status = %d, body = %s]", statusCode, string(body)) + return fmt.Errorf("HTTP request failed. [Status code]: %d, [Response]: %s", statusCode, string(body)) } return nil @@ -232,19 +233,23 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami } critical := -1.0 - if value, present := metric.GetTag("critical"); present { + value, criticalPresent := metric.GetTag("critical") + if criticalPresent { if s, err := strconv.ParseFloat(value, 64); err == nil { critical = s } } warning := -1.0 - if value, present := metric.GetTag("warning"); present { + 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, @@ -252,29 +257,34 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami Owner: resource, }, Status: transit.MonitorStatus(status), - LastCheckTime: transit.MillisecondTimestamp{Time: metric.Time()}, + LastCheckTime: lastCheckTime, LastPlugInOutput: message, Metrics: nil, } + var err error for _, value := range metric.FieldList() { var thresholds []transit.ThresholdValue - thresholds = append(thresholds, transit.ThresholdValue{ - SampleType: transit.Warning, - Label: value.Key + "_wn", - Value: &transit.TypedValue{ - ValueType: transit.DoubleType, - DoubleValue: warning, - }, - }) - thresholds = append(thresholds, transit.ThresholdValue{ - SampleType: transit.Critical, - Label: value.Key + "_cr", - Value: &transit.TypedValue{ - ValueType: transit.DoubleType, - DoubleValue: critical, - }, - }) + 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, + }, + }) + } valueType := transit.DoubleType var floatVal float64 @@ -286,13 +296,19 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami tmpStr := value.Value.(string) stringVal = tmpStr default: - floatVal, _ = internal.ToFloat64(value.Value) + floatVal, err = internal.ToFloat64(value.Value) + if err != nil { + g.Log.Warnf("could not convert %s to float: %v", value.Key, err) + } } + + endTime := transit.NewTimestamp() + endTime.Time = metric.Time() serviceObject.Metrics = append(serviceObject.Metrics, transit.TimeSeries{ MetricName: value.Key, SampleType: transit.Value, Interval: &transit.TimeInterval{ - EndTime: transit.MillisecondTimestamp{Time: metric.Time()}, + EndTime: endTime, }, Value: &transit.TypedValue{ ValueType: valueType, @@ -305,8 +321,8 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami } if !statusPresent { - var err error if serviceObject.Status, err = transit.CalculateServiceStatus(&serviceObject.Metrics); err != nil { + g.Log.Infof("could not calculate service status, reverting to default_service_state: %v", err) serviceObject.Status = transit.MonitorStatus(g.DefaultServiceState) } } @@ -331,7 +347,7 @@ func login(url, username, password string) ([]byte, error) { return nil, err } if statusCode != 200 { - return nil, fmt.Errorf("[ERROR]: Http request failed. [Status code]: %d, [Response]: %s", statusCode, string(body)) + return nil, fmt.Errorf("request failed with status-code %d: %v", statusCode, string(body)) } return body, nil From 793a8497cc0a69f73105577a0874309125c2543e Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Fri, 26 Nov 2021 14:34:24 +0300 Subject: [PATCH 09/11] feat: provide minor fixes --- go.mod | 2 +- go.sum | 4 +- plugins/outputs/groundwork/groundwork.go | 66 ++++++++----------- plugins/outputs/groundwork/groundwork_test.go | 26 ++++---- 4 files changed, 42 insertions(+), 56 deletions(-) diff --git a/go.mod b/go.mod index b2d7530d8663a..02ccd588e491b 100644 --- a/go.mod +++ b/go.mod @@ -133,7 +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-20211125122009-24221bf01623 + github.com/gwos/tcg/sdk v0.0.0-20211126104958-5206dbbe3426 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 diff --git a/go.sum b/go.sum index 667e100fb6c9b..87bdee34cc1ef 100644 --- a/go.sum +++ b/go.sum @@ -1145,8 +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-20211125122009-24221bf01623 h1:WiBrZ6coNJJhqJeaUpWT4jVSMJoIjKa+wumRUhLQy9Y= -github.com/gwos/tcg/sdk v0.0.0-20211125122009-24221bf01623/go.mod h1:OjlJNRXwlEjznVfU3YtLWH8FyM7KWHUevXDI47UeZeM= +github.com/gwos/tcg/sdk v0.0.0-20211126104958-5206dbbe3426 h1:iz9fJSFbmHvUhv+otAeKYvUky/xKUf5JhRQk1jC7iOw= +github.com/gwos/tcg/sdk v0.0.0-20211126104958-5206dbbe3426/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= diff --git a/plugins/outputs/groundwork/groundwork.go b/plugins/outputs/groundwork/groundwork.go index 640c92ff40ae7..a3f19526730d9 100644 --- a/plugins/outputs/groundwork/groundwork.go +++ b/plugins/outputs/groundwork/groundwork.go @@ -25,7 +25,7 @@ const ( logoutURL = "/api/auth/logout" ) -var ( +const ( sampleConfig = ` ## URL of your groundwork instance. url = "https://groundwork.example.com" @@ -49,16 +49,16 @@ var ( ) 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"` - authToken string - - Log telegraf.Logger `toml:"-"` + 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:"-"` + + authToken string } func (g *Groundwork) SampleConfig() string { @@ -142,7 +142,10 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { }) } - traceToken, _ := uuid.GenerateUUID() + traceToken, err := uuid.GenerateUUID() + if err != nil { + return err + } requestJSON, err := json.Marshal(transit.DynamicResourcesWithServicesRequest{ Context: &transit.TracerContext{ AppType: "TELEGRAF", @@ -222,10 +225,7 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami status = value } - message := "" - if value, present := metric.GetTag("message"); present { - message = value - } + message, _ := metric.GetTag("message") unitType := string(transit.UnitCounter) if value, present := metric.GetTag("unitType"); present { @@ -262,7 +262,6 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami Metrics: nil, } - var err error for _, value := range metric.FieldList() { var thresholds []transit.ThresholdValue if warningPresent { @@ -286,45 +285,32 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami }) } - valueType := transit.DoubleType - var floatVal float64 - var stringVal string - - switch value.Value.(type) { - case string: - valueType = transit.StringType - tmpStr := value.Value.(string) - stringVal = tmpStr - default: - floatVal, err = internal.ToFloat64(value.Value) - if err != nil { - g.Log.Warnf("could not convert %s to float: %v", value.Key, err) - } + typedValue := new(transit.TypedValue) + err := typedValue.FromInterface(value.Value) + if err != nil { + typedValue = nil + g.Log.Errorf("%v", err) } - endTime := transit.NewTimestamp() - endTime.Time = metric.Time() serviceObject.Metrics = append(serviceObject.Metrics, transit.TimeSeries{ MetricName: value.Key, SampleType: transit.Value, Interval: &transit.TimeInterval{ - EndTime: endTime, - }, - Value: &transit.TypedValue{ - ValueType: valueType, - DoubleValue: floatVal, - StringValue: stringVal, + EndTime: lastCheckTime, }, + Value: typedValue, Unit: transit.UnitType(unitType), Thresholds: &thresholds, }) } if !statusPresent { - if serviceObject.Status, err = transit.CalculateServiceStatus(&serviceObject.Metrics); err != nil { + 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 diff --git a/plugins/outputs/groundwork/groundwork_test.go b/plugins/outputs/groundwork/groundwork_test.go index 28b24339dd05d..bc4c4e5bc61a5 100644 --- a/plugins/outputs/groundwork/groundwork_test.go +++ b/plugins/outputs/groundwork/groundwork_test.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/testutil" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "io/ioutil" "net/http" "net/http/httptest" @@ -19,40 +19,40 @@ const ( func TestWrite(t *testing.T) { // Generate test metric with default name to test Write logic - floatMetric := testutil.TestMetric(1, "Float") + 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) - assert.NoError(t, err) + require.NoError(t, err) // Decode body to use in assertations below - var obj GroundworkObject + var obj groundworkObject err = json.Unmarshal(body, &obj) - assert.NoError(t, err) + require.NoError(t, err) // Check if server gets valid metrics object - assert.Equal(t, obj.Context.AgentID, defaultTestAgentID) - assert.Equal(t, obj.Resources[0].Name, defaultHost) - assert.Equal( + 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", ) - assert.Equal( + require.Equal( t, obj.Resources[0].Services[0].Metrics[0].Value.DoubleValue, 1.0, ) - assert.Equal( + require.Equal( t, obj.Resources[0].Services[1].Metrics[0].Value.StringValue, "Test", ) _, err = fmt.Fprintln(w, `OK`) - assert.NoError(t, err) + require.NoError(t, err) })) i := Groundwork{ @@ -62,12 +62,12 @@ func TestWrite(t *testing.T) { } err := i.Write([]telegraf.Metric{floatMetric, stringMetric}) - assert.NoError(t, err) + require.NoError(t, err) defer server.Close() } -type GroundworkObject struct { +type groundworkObject struct { Context struct { AgentID string `json:"agentId"` } `json:"context"` From fae55d292701e56e90be847efe6a124ffdf8f020 Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Mon, 29 Nov 2021 15:19:37 +0300 Subject: [PATCH 10/11] feat: add GWClient --- plugins/outputs/groundwork/README.md | 4 +- plugins/outputs/groundwork/groundwork.go | 122 +++++------------- plugins/outputs/groundwork/groundwork_test.go | 16 ++- 3 files changed, 46 insertions(+), 96 deletions(-) diff --git a/plugins/outputs/groundwork/README.md b/plugins/outputs/groundwork/README.md index 235753ed53be7..ea0fc92fc8248 100644 --- a/plugins/outputs/groundwork/README.md +++ b/plugins/outputs/groundwork/README.md @@ -4,7 +4,7 @@ This plugin writes to a [GroundWork Monitor][1] instance. Plugin only supports G [1]: https://www.gwos.com/product/groundwork-monitor/ -### Configuration +## Configuration ```toml [[outputs.groundwork]] @@ -28,7 +28,7 @@ This plugin writes to a [GroundWork Monitor][1] instance. Plugin only supports G # resource_tag = "host" ``` -### List of tags used by the plugin +## 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. diff --git a/plugins/outputs/groundwork/groundwork.go b/plugins/outputs/groundwork/groundwork.go index a3f19526730d9..ec11439b8cc45 100644 --- a/plugins/outputs/groundwork/groundwork.go +++ b/plugins/outputs/groundwork/groundwork.go @@ -1,32 +1,21 @@ package groundwork import ( + "context" "encoding/json" "errors" "fmt" - "net/http" "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/internal" "github.com/influxdata/telegraf/plugins/outputs" ) -const ( - defaultMonitoringRoute = "/api/monitoring?dynamic=true" -) - -// Login and logout routes from Groundwork API -const ( - loginURL = "/api/auth/login" - logoutURL = "/api/auth/logout" -) - -const ( - sampleConfig = ` +const sampleConfig = ` ## URL of your groundwork instance. url = "https://groundwork.example.com" @@ -46,7 +35,6 @@ const ( ## The name of the tag that contains the hostname. # resource_tag = "host" ` -) type Groundwork struct { Server string `toml:"url"` @@ -57,8 +45,7 @@ type Groundwork struct { DefaultServiceState string `toml:"default_service_state"` ResourceTag string `toml:"resource_tag"` Log telegraf.Logger `toml:"-"` - - authToken string + client clients.GWClient } func (g *Groundwork) SampleConfig() string { @@ -88,43 +75,44 @@ func (g *Groundwork) Init() error { 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 { - byteToken, err := login(g.Server+loginURL, g.Username, g.Password) + err := g.client.Connect() if err != nil { - return fmt.Errorf("could not log in at %s: %v", g.Server+loginURL, err) + return fmt.Errorf("could not log in: %v", err) } - - g.authToken = string(byteToken) - return nil } func (g *Groundwork) Close() error { - formValues := map[string]string{ - "gwos-app-name": "telegraf", - "gwos-api-token": g.authToken, - } - - headers := map[string]string{ - "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": internal.ProductToken(), - } - - if _, _, err := clients.SendRequest(http.MethodPost, g.Server+logoutURL, headers, formValues, nil); err != nil { - return fmt.Errorf("could not log out at %s: %v", g.Server+logoutURL, err) + 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 := g.parseMetric(metric) - resourceToServicesMap[resource] = append(resourceToServicesMap[resource], service) + 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 @@ -162,35 +150,11 @@ func (g *Groundwork) Write(metrics []telegraf.Metric) error { return err } - headers := map[string]string{ - "GWOS-APP-NAME": "telegraf", - "GWOS-API-TOKEN": g.authToken, - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": internal.ProductToken(), - } - - statusCode, body, err := clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) + _, err = g.client.SendResourcesWithMetrics(context.Background(), requestJSON) if err != nil { return fmt.Errorf("error while sending: %v", err) } - /* Re-login mechanism */ - if statusCode == 401 { - if err = g.Connect(); err != nil { - return fmt.Errorf("re-login failed: %v", err) - } - headers["GWOS-API-TOKEN"] = g.authToken - statusCode, body, err = clients.SendRequest(http.MethodPost, g.Server+defaultMonitoringRoute, headers, nil, requestJSON) - if err != nil { - return fmt.Errorf("error while sending: %v", err) - } - } - - if statusCode != 200 { - return fmt.Errorf("HTTP request failed. [Status code]: %d, [Response]: %s", statusCode, string(body)) - } - return nil } @@ -208,7 +172,7 @@ func init() { }) } -func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.DynamicMonitoredService) { +func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, *transit.DynamicMonitoredService, error) { resource := g.DefaultHost if value, present := metric.GetTag(g.ResourceTag); present { resource = value @@ -232,7 +196,7 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami unitType = value } - critical := -1.0 + var critical float64 value, criticalPresent := metric.GetTag("critical") if criticalPresent { if s, err := strconv.ParseFloat(value, 64); err == nil { @@ -240,7 +204,7 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami } } - warning := -1.0 + var warning float64 value, warningPresent := metric.GetTag("warning") if warningPresent { if s, err := strconv.ParseFloat(value, 64); err == nil { @@ -288,8 +252,7 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami typedValue := new(transit.TypedValue) err := typedValue.FromInterface(value.Value) if err != nil { - typedValue = nil - g.Log.Errorf("%v", err) + return "", nil, err } serviceObject.Metrics = append(serviceObject.Metrics, transit.TimeSeries{ @@ -313,30 +276,7 @@ func (g *Groundwork) parseMetric(metric telegraf.Metric) (string, transit.Dynami serviceObject.Status = serviceStatus } - return resource, serviceObject -} - -func login(url, username, password string) ([]byte, error) { - formValues := map[string]string{ - "user": username, - "password": password, - "gwos-app-name": "telegraf", - } - headers := map[string]string{ - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "text/plain", - "User-Agent": internal.ProductToken(), - } - - statusCode, body, err := clients.SendRequest(http.MethodPost, url, headers, formValues, nil) - if err != nil { - return nil, err - } - if statusCode != 200 { - return nil, fmt.Errorf("request failed with status-code %d: %v", statusCode, string(body)) - } - - return body, nil + return resource, &serviceObject, nil } func validStatus(status string) bool { diff --git a/plugins/outputs/groundwork/groundwork_test.go b/plugins/outputs/groundwork/groundwork_test.go index bc4c4e5bc61a5..16ae1f057501f 100644 --- a/plugins/outputs/groundwork/groundwork_test.go +++ b/plugins/outputs/groundwork/groundwork_test.go @@ -3,13 +3,16 @@ package groundwork import ( "encoding/json" "fmt" - "github.com/influxdata/telegraf" - "github.com/influxdata/telegraf/testutil" - "github.com/stretchr/testify/require" "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 ( @@ -59,6 +62,13 @@ func TestWrite(t *testing.T) { 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}) From 8c944039bdcf71285a0edb1f2112a6112a1668bc Mon Sep 17 00:00:00 2001 From: VladislavSenkevich Date: Tue, 30 Nov 2021 20:56:36 +0300 Subject: [PATCH 11/11] feat: update LICENSE --- docs/LICENSE_OF_DEPENDENCIES.md | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index 5904b938f19d3..09c1b72382150 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -115,7 +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 [TCG License](https://github.com/gwos/tcg/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) diff --git a/go.mod b/go.mod index 02ccd588e491b..889d1026004dc 100644 --- a/go.mod +++ b/go.mod @@ -133,7 +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-20211126104958-5206dbbe3426 + 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 diff --git a/go.sum b/go.sum index 87bdee34cc1ef..d64621e1cef20 100644 --- a/go.sum +++ b/go.sum @@ -1145,8 +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-20211126104958-5206dbbe3426 h1:iz9fJSFbmHvUhv+otAeKYvUky/xKUf5JhRQk1jC7iOw= -github.com/gwos/tcg/sdk v0.0.0-20211126104958-5206dbbe3426/go.mod h1:OjlJNRXwlEjznVfU3YtLWH8FyM7KWHUevXDI47UeZeM= +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=