1
0
Fork 0
telegraf/plugins/inputs/ctrlx_datalayer/ctrlx_datalayer.go

372 lines
10 KiB
Go
Raw Normal View History

//go:generate ../../../tools/readme_config_includer/generator
package ctrlx_datalayer
import (
"bytes"
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/boschrexroth/ctrlx-datalayer-golang/pkg/sseclient"
"github.com/boschrexroth/ctrlx-datalayer-golang/pkg/token"
"github.com/google/uuid"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal/choice"
"github.com/influxdata/telegraf/metric"
common_http "github.com/influxdata/telegraf/plugins/common/http"
"github.com/influxdata/telegraf/plugins/inputs"
parsers_json "github.com/influxdata/telegraf/plugins/parsers/json"
)
// This plugin is based on the official ctrlX CORE API. Documentation can be found in OpenAPI format at:
// https://boschrexroth.github.io/rest-api-description/ctrlx-automation/ctrlx-core/
// Used APIs are:
// * ctrlX CORE - Authorization and Authentication API
// * ctrlX CORE - Data Layer API
//
// All communication between the device and this input plugin is based
// on https REST and HTML5 Server Sent Events (sse).
//go:embed sample.conf
var sampleConfig string
type CtrlXDataLayer struct {
Server string `toml:"server"`
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
Log telegraf.Logger `toml:"-"`
Subscription []subscription
url string
wg sync.WaitGroup
cancel context.CancelFunc
acc telegraf.Accumulator
connection *http.Client
tokenManager token.TokenManager
common_http.HTTPClientConfig
}
func (*CtrlXDataLayer) SampleConfig() string {
return sampleConfig
}
func (c *CtrlXDataLayer) Init() error {
// Check all configured subscriptions for valid settings
for i := range c.Subscription {
sub := &c.Subscription[i]
sub.applyDefaultSettings()
if !choice.Contains(sub.QueueBehaviour, queueBehaviours) {
c.Log.Infof("The right queue behaviour values are %v", queueBehaviours)
return fmt.Errorf("subscription %d: setting 'queue_behaviour' %q is invalid", i, sub.QueueBehaviour)
}
if !choice.Contains(sub.ValueChange, valueChanges) {
c.Log.Infof("The right value change values are %v", valueChanges)
return fmt.Errorf("subscription %d: setting 'value_change' %q is invalid", i, sub.ValueChange)
}
if len(sub.Nodes) == 0 {
c.Log.Warn("A configured subscription has no nodes configured")
}
sub.index = i
}
// Generate valid communication url based on configured server address
u := url.URL{
Scheme: "https",
Host: c.Server,
}
c.url = u.String()
if _, err := url.Parse(c.url); err != nil {
return errors.New("invalid server address")
}
return nil
}
func (c *CtrlXDataLayer) Start(acc telegraf.Accumulator) error {
var ctx context.Context
ctx, c.cancel = context.WithCancel(context.Background())
var err error
c.connection, err = c.HTTPClientConfig.CreateClient(ctx, c.Log)
if err != nil {
return fmt.Errorf("failed to create http client: %w", err)
}
username, err := c.Username.Get()
if err != nil {
return fmt.Errorf("getting username failed: %w", err)
}
password, err := c.Password.Get()
if err != nil {
username.Destroy()
return fmt.Errorf("getting password failed: %w", err)
}
c.tokenManager = token.TokenManager{
Url: c.url,
Username: username.String(),
Password: password.String(),
Connection: c.connection,
}
username.Destroy()
password.Destroy()
c.acc = acc
c.gatherLoop(ctx)
return nil
}
func (*CtrlXDataLayer) Gather(telegraf.Accumulator) error {
// Metrics are sent to the accumulator asynchronously in worker thread. So nothing to do here.
return nil
}
func (c *CtrlXDataLayer) Stop() {
c.cancel()
c.wg.Wait()
if c.connection != nil {
c.connection.CloseIdleConnections()
}
}
// convertTimestamp2UnixTime converts the given Data Layer timestamp of the payload to UnixTime.
func convertTimestamp2UnixTime(t int64) time.Time {
// 1 sec=1000 millisec=1000000 microsec=1000000000 nanosec.
// Convert from FILETIME (100-nanosecond intervals since January 1, 1601 UTC) to
// seconds and nanoseconds since January 1, 1970 UTC.
// Between Jan 1, 1601 and Jan 1, 1970 there are 11644473600 seconds.
return time.Unix(0, (t-116444736000000000)*100)
}
// createSubscription uses the official 'ctrlX Data Layer API' to create the sse subscription.
func (c *CtrlXDataLayer) createSubscription(sub *subscription) (string, error) {
sseURL := c.url + subscriptionPath
id := "telegraf_" + uuid.New().String()
request := sub.createRequest(id)
payload, err := json.Marshal(request)
if err != nil {
return "", fmt.Errorf("failed to create subscription %d payload: %w", sub.index, err)
}
requestBody := bytes.NewBuffer(payload)
req, err := http.NewRequest("POST", sseURL, requestBody)
if err != nil {
return "", fmt.Errorf("failed to create subscription %d request: %w", sub.index, err)
}
req.Header.Add("Authorization", c.tokenManager.Token.String())
resp, err := c.connection.Do(req)
if err != nil {
return "", fmt.Errorf("failed to do request to create sse subscription %d: %w", sub.index, err)
}
resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 201 {
return "", fmt.Errorf("failed to create sse subscription %d, status: %s", sub.index, resp.Status)
}
return sseURL + "/" + id, nil
}
// createSubscriptionAndSseClient creates a sse subscription on the server and
// initializes a sse client to receive sse events from the server.
func (c *CtrlXDataLayer) createSubscriptionAndSseClient(sub *subscription) (*sseclient.SseClient, error) {
t, err := c.tokenManager.RequestAuthToken()
if err != nil {
return nil, err
}
subURL, err := c.createSubscription(sub)
if err != nil {
return nil, err
}
client := sseclient.NewSseClient(subURL, t.String(), c.InsecureSkipVerify)
return client, nil
}
// addMetric writes sse metric into accumulator.
func (c *CtrlXDataLayer) addMetric(se *sseclient.SseEvent, sub *subscription) {
switch se.Event {
case "update":
// Received an updated value, that we translate into a metric
var d sseEventData
if err := json.Unmarshal([]byte(se.Data), &d); err != nil {
c.acc.AddError(fmt.Errorf("received malformed data from 'update' event: %w", err))
return
}
m, err := c.createMetric(&d, sub)
if err != nil {
c.acc.AddError(fmt.Errorf("failed to create metrics: %w", err))
return
}
c.acc.AddMetric(m)
case "error":
// Received an error event, that we report to the accumulator
var e sseEventError
if err := json.Unmarshal([]byte(se.Data), &e); err != nil {
c.acc.AddError(fmt.Errorf("received malformed data from 'error' event: %w", err))
return
}
c.acc.AddError(fmt.Errorf("received 'error' event for node: %q", e.Instance))
case "keepalive":
// Keepalive events are ignored for the moment
c.Log.Debug("Received keepalive event")
default:
// Received a yet unsupported event type
c.Log.Debugf("Received unsupported event: %q", se.Event)
}
}
// createMetric - create metric depending on flag 'output_json' and data type
func (c *CtrlXDataLayer) createMetric(em *sseEventData, sub *subscription) (telegraf.Metric, error) {
t := convertTimestamp2UnixTime(em.Timestamp)
node := sub.node(em.Node)
if node == nil {
return nil, errors.New("node not found")
}
// default tags
tags := map[string]string{
"node": em.Node,
"source": c.Server,
}
// add tags of subscription if user has defined
for key, value := range sub.Tags {
tags[key] = value
}
// add tags of node if user has defined
for key, value := range node.Tags {
tags[key] = value
}
// set measurement of subscription
measurement := sub.Measurement
// get field key from node properties
fieldKey := node.fieldKey()
if fieldKey == "" {
return nil, errors.New("field key not valid")
}
if sub.OutputJSONString {
b, err := json.Marshal(em.Value)
if err != nil {
return nil, err
}
fields := map[string]interface{}{fieldKey: string(b)}
m := metric.New(measurement, tags, fields, t)
return m, nil
}
switch em.Type {
case "object":
flattener := parsers_json.JSONFlattener{}
err := flattener.FullFlattenJSON(fieldKey, em.Value, true, true)
if err != nil {
return nil, err
}
m := metric.New(measurement, tags, flattener.Fields, t)
return m, nil
case "arbool8",
"arint8", "aruint8",
"arint16", "aruint16",
"arint32", "aruint32",
"arint64", "aruint64",
"arfloat", "ardouble",
"arstring",
"artimestamp":
fields := make(map[string]interface{})
values := em.Value.([]interface{})
for i := 0; i < len(values); i++ {
index := strconv.Itoa(i)
key := fieldKey + "_" + index
fields[key] = values[i]
}
m := metric.New(measurement, tags, fields, t)
return m, nil
case "bool8",
"int8", "uint8",
"int16", "uint16",
"int32", "uint32",
"int64", "uint64",
"float", "double",
"string",
"timestamp":
fields := map[string]interface{}{fieldKey: em.Value}
m := metric.New(measurement, tags, fields, t)
return m, nil
}
return nil, fmt.Errorf("unsupported value type: %s", em.Type)
}
// gatherLoop creates sse subscriptions on the Data Layer and requests the sse data
// the connection will be restablished if the sse subscription is broken.
func (c *CtrlXDataLayer) gatherLoop(ctx context.Context) {
for _, sub := range c.Subscription {
c.wg.Add(1)
go func(sub subscription) {
defer c.wg.Done()
for {
select {
case <-ctx.Done():
c.Log.Debugf("Gather loop for subscription %d stopped", sub.index)
return
default:
client, err := c.createSubscriptionAndSseClient(&sub)
if err != nil {
c.Log.Errorf("Creating sse client to subscription %d: %v", sub.index, err)
time.Sleep(time.Duration(defaultReconnectInterval))
continue
}
c.Log.Debugf("Created sse client to subscription %d", sub.index)
// Establish connection and handle events in a callback function.
err = client.Subscribe(ctx, func(event string, data string) {
c.addMetric(&sseclient.SseEvent{
Event: event,
Data: data,
}, &sub)
})
if errors.Is(err, context.Canceled) {
// Subscription cancelled
c.Log.Debugf("Requesting data of subscription %d cancelled", sub.index)
return
}
c.Log.Errorf("Requesting data of subscription %d failed: %v", sub.index, err)
}
}
}(sub)
}
}
// init registers the plugin in telegraf.
func init() {
inputs.Add("ctrlx_datalayer", func() telegraf.Input {
return &CtrlXDataLayer{}
})
}