Skip to content

Commit

Permalink
feat: Add Clarify output plugin
Browse files Browse the repository at this point in the history
Add plugin to support writing metrics to Clarify.
  • Loading branch information
bbergshaven committed May 2, 2023
1 parent 129d8eb commit 2e7a342
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ following works:
- github.com/cenkalti/backoff [MIT License](https://github.com/cenkalti/backoff/blob/master/LICENSE)
- github.com/cespare/xxhash [MIT License](https://github.com/cespare/xxhash/blob/master/LICENSE.txt)
- github.com/cisco-ie/nx-telemetry-proto [Apache License 2.0](https://github.com/cisco-ie/nx-telemetry-proto/blob/master/LICENSE)
- github.com/clarify/clarify-go [Apache License 2.0](https://github.com/clarify/clarify-go/blob/master/LICENSE)
- github.com/containerd/containerd [Apache License 2.0](https://github.com/containerd/containerd/blob/master/LICENSE)
- github.com/coocood/freecache [MIT License](https://github.com/coocood/freecache/blob/master/LICENSE)
- github.com/coreos/go-semver [Apache License 2.0](https://github.com/coreos/go-semver/blob/main/LICENSE)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ require (
github.com/blues/jsonata-go v1.5.4
github.com/bmatcuk/doublestar/v3 v3.0.0
github.com/caio/go-tdigest v3.1.0+incompatible
github.com/clarify/clarify-go v0.2.4
github.com/cisco-ie/nx-telemetry-proto v0.0.0-20230117155933-f64c045c77df
github.com/coocood/freecache v1.2.3
github.com/coreos/go-semver v0.3.1
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/cisco-ie/nx-telemetry-proto v0.0.0-20230117155933-f64c045c77df h1:GmrltUp5Qf5XhT+LmqMDizsgm/6VHTSxPWRdrq21yRo=
github.com/cisco-ie/nx-telemetry-proto v0.0.0-20230117155933-f64c045c77df/go.mod h1:rJDd05J5hqWVU9MjJ+5jw1CuLn/jRhvU0xtFEzzqjwM=
github.com/clarify/clarify-go v0.2.4 h1:4MH6UHS3PFSNeitAkS/k3ur6ASxZpiRa6EezkbCVLVs=
github.com/clarify/clarify-go v0.2.4/go.mod h1:bdKwACxI2WZMdlFQOun4J8H5wG0Dbn1bKWelgxsDJaM=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
Expand Down Expand Up @@ -2524,6 +2526,8 @@ golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8H
golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b h1:EqBVA+nNsObCwQoBEHy4wLU0pi7i8a4AL3pbItPdPkE=
golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
Expand Down
5 changes: 5 additions & 0 deletions plugins/outputs/all/clarify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !custom || outputs || outputs.clarify

package all

import _ "github.com/influxdata/telegraf/plugins/outputs/clarify" // register plugin
78 changes: 78 additions & 0 deletions plugins/outputs/clarify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Clarify Output Plugin

This plugin writes to [Clarify][clarify]. To use this plugin you will
need to obtain a set of [credentials][credentials].

## Global configuration options <!-- @/docs/includes/plugin_config.md -->

In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.

[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins

## Configuration

```toml @sample.conf
[[outputs.clarify]]
## Credentials File (OAuth 2.0 from Clarify integration).
credentials_file = "/path/to/clarify/credentials.json"

## Clarify username password (Basic Auth from Clarify integration).
username = "i-am-bob"
password = "secret-password"

## Tags to be included when generating the unique ID for a signal in Clarify.
id_tags = ['sensor']
```

You can use either a credentials file or username/password.
If both are present and valid in the configuration the
credentials file will be used.

## How Telegraf Metrics map to Clarify signals

Clarify signal names are formed by joining the Telegraf metric name and the
field key with a `.` character. Telegraf tags are added to signal labels.

If a tag named `clarify_input_id` is present as a tag and there is only one
field present in the metric, this tag will be used as the inputID in Clarify.

If information from one or several tags is needed to uniquely identify a metric
field, the id_tags array can be added to the config with the needed tag names.
E.g:

`id_tags = ['sensor']`

Clarify only supports values that can be converted to floating point numbers.
Strings and invalid numbers are ignored.

[clarify]: https://clarify.io
[clarifydoc]: https://docs.clarify.io
[credentials]: https://docs.clarify.io/users/admin/integrations/credentials

## Example parsing

The following input would be stored in Clarify with the values shown below:

```text
temperature,host=demo.clarifylocal,sensor=TC0P value=49 1682670910000000000
```

```json
"signal" {
"id": "temperature.value.TC0P"
"name": "temperature.value"
"labels": {
"host": ["demo.clarifylocal"],
"sensor": ["TC0P"]
}
}
"values" {
"times": ["2023-04-28T08:43:16+00:00"],
"series": {
"temperature.value.TC0P": [49]
}
}
```
154 changes: 154 additions & 0 deletions plugins/outputs/clarify/clarify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package clarify

import (
"context"
_ "embed"
"fmt"
"strings"

"github.com/clarify/clarify-go"
"github.com/clarify/clarify-go/fields"
"github.com/clarify/clarify-go/views"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/outputs"
)

type Clarify struct {
Username string `toml:"username"`
Password string `toml:"password"`
CredentialsFile string `toml:"credentials_file"`
IDTags []string `toml:"id_tags"`
Log telegraf.Logger `toml:"-"`

client *clarify.Client
}

//go:embed sample.conf
var sampleConfig string

func (c *Clarify) Connect() error {
ctx := context.Background()
if c.CredentialsFile != "" {
creds, err := clarify.CredentialsFromFile(c.CredentialsFile)
if err != nil {
return err
}
c.client = creds.Client(ctx)
return nil
}
if c.Username != "" && c.Password != "" {
creds := clarify.BasicAuthCredentials(c.Username, c.Password)
c.client = creds.Client(ctx)
return nil
}
return fmt.Errorf("no Clarify credentials provided")
}

func verifyValue(v interface{}) (float64, error) {
var value float64
switch v := v.(type) {
case bool:
value = float64(0)
if v {
value = float64(1)
}
case uint8:
value = float64(v)
case uint16:
value = float64(v)
case uint32:
value = float64(v)
case uint64:
value = float64(v)
case int8:
value = float64(v)
case int16:
value = float64(v)
case int32:
value = float64(v)
case int64:
value = float64(v)
case float32:
value = float64(v)
case float64:
value = v
default:
return value, fmt.Errorf("unsupported field type: %T", v)
}
return value, nil
}

func (c *Clarify) Write(metrics []telegraf.Metric) error {
signals := make(map[string]views.SignalSave)
frame := views.DataFrame{}

for _, m := range metrics {
for _, f := range m.FieldList() {
if value, err := verifyValue(f.Value); err == nil {
id := c.generateID(m, f)
ts := fields.AsTimestamp(m.Time())

if _, ok := frame[id]; ok {
frame[id][ts] = value
} else {
frame[id] = views.DataSeries{ts: value}
}

s := views.SignalSave{}
s.Name = fmt.Sprintf("%s.%s", m.Name(), f.Key)

for _, t := range m.TagList() {
labelName := strings.ReplaceAll(t.Key, " ", "-")
labelName = strings.ReplaceAll(labelName, "_", "-")
labelName = strings.ToLower(labelName)
s.Labels.Add(labelName, t.Value)
}

signals[id] = s
} else {
c.Log.Infof("Unable to add field `%s` for metric `%s` due to error '%v', skipping", f.Key, m.Name(), err)
}
}
}

if _, err := c.client.Insert(frame).Do(context.Background()); err != nil {
return err
}

if _, err := c.client.SaveSignals(signals).Do(context.Background()); err != nil {
return err
}

return nil
}

func (c *Clarify) generateID(m telegraf.Metric, f *telegraf.Field) string {
var id string
cid, exist := m.GetTag("clarify_input_id")
if exist && len(m.FieldList()) == 1 {
id = cid
} else {
id = fmt.Sprintf("%s.%s", m.Name(), f.Key)
for _, idTag := range c.IDTags {
if m.HasTag(idTag) {
id = fmt.Sprintf("%s.%s", id, m.Tags()[idTag])
}
}
}
return strings.ToLower(id)
}

func (c *Clarify) SampleConfig() string {
return sampleConfig
}

func (c *Clarify) Close() error {
c.client = nil
return nil
}

func init() {
outputs.Add("clarify", func() telegraf.Output {
return &Clarify{}
})
}
9 changes: 9 additions & 0 deletions plugins/outputs/clarify/sample.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Credentials File (Oauth2 from Clarify Integration)
credentials_file = "/path/to/clarify/credentials.json"

## Clarify username password (Basic Auth from Clarify Integration)
username = "i-am-bob"
password = "secret-password"

## Tags to be included when generating the unique ID for a signal in Clarify
id_tags = ['sensor']

0 comments on commit 2e7a342

Please sign in to comment.