Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
371
plugins/inputs/ctrlx_datalayer/ctrlx_datalayer.go
Normal file
371
plugins/inputs/ctrlx_datalayer/ctrlx_datalayer.go
Normal file
|
@ -0,0 +1,371 @@
|
|||
//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{}
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue