Use line protocol encoder

This commit is contained in:
Holger Obermaier 2023-10-09 10:12:14 +02:00
parent fd1cdc5c07
commit a4d7593af5
2 changed files with 89 additions and 13 deletions

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings"
"sync" "sync"
"time" "time"
@ -13,7 +14,8 @@ import (
lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric" lp "github.com/ClusterCockpit/cc-metric-collector/pkg/ccMetric"
influxdb2 "github.com/influxdata/influxdb-client-go/v2" influxdb2 "github.com/influxdata/influxdb-client-go/v2"
influxdb2Api "github.com/influxdata/influxdb-client-go/v2/api" influxdb2Api "github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/api/write" influx "github.com/influxdata/line-protocol/v2/lineprotocol"
"golang.org/x/exp/slices"
) )
type InfluxSink struct { type InfluxSink struct {
@ -52,11 +54,15 @@ type InfluxSink struct {
// maximum total retry timeout // maximum total retry timeout
InfluxMaxRetryTime string `json:"max_retry_time,omitempty"` InfluxMaxRetryTime string `json:"max_retry_time,omitempty"`
} }
batch []*write.Point batch []string
flushTimer *time.Timer flushTimer *time.Timer
flushDelay time.Duration flushDelay time.Duration
batchMutex sync.Mutex // Flush() runs in another goroutine, so this lock has to protect the buffer batchMutex sync.Mutex // Flush() runs in another goroutine, so this lock has to protect the buffer
flushTimerMutex sync.Mutex // Ensure only one flush timer is running flushTimerMutex sync.Mutex // Ensure only one flush timer is running
// influx line protocol encoder
encoder influx.Encoder
// List of tags and meta data tags which should be used as tags
extended_tag_list []key_value_pair
} }
// connect connects to the InfluxDB server // connect connects to the InfluxDB server
@ -130,7 +136,8 @@ func (s *InfluxSink) connect() error {
}, },
) )
clientOptions.SetPrecision(time.Second) // Set time precision
clientOptions.SetPrecision(time.Nanosecond)
// Create new writeAPI // Create new writeAPI
s.client = influxdb2.NewClientWithOptions(uri, auth, clientOptions) s.client = influxdb2.NewClientWithOptions(uri, auth, clientOptions)
@ -186,15 +193,80 @@ func (s *InfluxSink) Write(m lp.CCMetric) error {
s.batch[i] = s.batch[i+s.config.DropRate] s.batch[i] = s.batch[i+s.config.DropRate]
} }
for i := newSize; i < s.config.BatchSize; i++ { for i := newSize; i < s.config.BatchSize; i++ {
s.batch[i] = nil s.batch[i] = ""
} }
s.batch = s.batch[:newSize] s.batch = s.batch[:newSize]
cclog.ComponentError(s.name, "Batch slice full, dropping", s.config.DropRate, "oldest metric(s)") cclog.ComponentError(s.name, "Batch slice full, dropping", s.config.DropRate, "oldest metric(s)")
} }
// Encode measurement name
s.encoder.StartLine(m.Name())
// copy tags and meta data which should be used as tags
s.extended_tag_list = s.extended_tag_list[:0]
for key, value := range m.Tags() {
s.extended_tag_list =
append(
s.extended_tag_list,
key_value_pair{
key: key,
value: value,
},
)
}
for _, key := range s.config.MetaAsTags {
if value, ok := m.GetMeta(key); ok {
s.extended_tag_list =
append(
s.extended_tag_list,
key_value_pair{
key: key,
value: value,
},
)
}
}
// Encode tags (they musts be in lexical order)
slices.SortFunc(
s.extended_tag_list,
func(a key_value_pair, b key_value_pair) int {
if a.key < b.key {
return -1
}
if a.key > b.key {
return +1
}
return 0
},
)
for i := range s.extended_tag_list {
s.encoder.AddTag(
s.extended_tag_list[i].key,
s.extended_tag_list[i].value,
)
}
// Encode fields
for key, value := range m.Fields() {
s.encoder.AddField(key, influx.MustNewValue(value))
}
// Encode time stamp
s.encoder.EndLine(m.Time())
// Check that encoding worked
if err := s.encoder.Err(); err != nil {
cclog.ComponentError(s.name, "Write(): Encoding failed:", err)
return err
}
// Append metric to batch slice // Append metric to batch slice
p := m.ToPoint(s.meta_as_tags) s.batch = append(s.batch,
s.batch = append(s.batch, p) string(
slices.Clone(
s.encoder.Bytes())))
s.encoder.Reset()
// Flush synchronously if "flush_delay" is zero // Flush synchronously if "flush_delay" is zero
// or // or
@ -225,7 +297,7 @@ func (s *InfluxSink) Flush() error {
} }
// Send metrics from batch slice // Send metrics from batch slice
err := s.writeApi.WritePoint(context.Background(), s.batch...) err := s.writeApi.WriteRecord(context.Background(), strings.Join(s.batch, ""))
if err != nil { if err != nil {
cclog.ComponentError(s.name, "Flush(): Flush of", len(s.batch), "metrics failed:", err) cclog.ComponentError(s.name, "Flush(): Flush of", len(s.batch), "metrics failed:", err)
return err return err
@ -233,7 +305,7 @@ func (s *InfluxSink) Flush() error {
// Clear batch slice // Clear batch slice
for i := range s.batch { for i := range s.batch {
s.batch[i] = nil s.batch[i] = ""
} }
s.batch = s.batch[:0] s.batch = s.batch[:0]
@ -311,11 +383,16 @@ func NewInfluxSink(name string, config json.RawMessage) (Sink, error) {
} }
// allocate batch slice // allocate batch slice
s.batch = make([]*write.Point, 0, s.config.BatchSize) s.batch = make([]string, 0, s.config.BatchSize)
// Connect to InfluxDB server // Connect to InfluxDB server
if err := s.connect(); err != nil { if err := s.connect(); err != nil {
return s, fmt.Errorf("unable to connect: %v", err) return s, fmt.Errorf("unable to connect: %v", err)
} }
// Configure influx line protocol encoder
s.encoder.SetPrecision(influx.Nanosecond)
s.extended_tag_list = make([]key_value_pair, 0)
return s, nil return s, nil
} }

View File

@ -2,7 +2,6 @@
The `influxdb` sink uses the official [InfluxDB golang client](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2) to write the metrics to an InfluxDB database in a **blocking** fashion. It provides only support for V2 write endpoints (InfluxDB 1.8.0 or later). The `influxdb` sink uses the official [InfluxDB golang client](https://pkg.go.dev/github.com/influxdata/influxdb-client-go/v2) to write the metrics to an InfluxDB database in a **blocking** fashion. It provides only support for V2 write endpoints (InfluxDB 1.8.0 or later).
### Configuration structure ### Configuration structure
```json ```json
@ -18,14 +17,14 @@ The `influxdb` sink uses the official [InfluxDB golang client](https://pkg.go.de
"organization": "myorg", "organization": "myorg",
"ssl": true, "ssl": true,
"flush_delay" : "1s", "flush_delay" : "1s",
"batch_size" : 100 "batch_size" : 1000
} }
} }
``` ```
- `type`: makes the sink an `influxdb` sink - `type`: makes the sink an `influxdb` sink
- `meta_as_tags`: print all meta information as tags in the output (optional) - `meta_as_tags`: print all meta information as tags in the output (optional)
- `database`: All metrics are written to this bucket - `database`: All metrics are written to this bucket
- `host`: Hostname of the InfluxDB database server - `host`: Hostname of the InfluxDB database server
- `port`: Portnumber (as string) of the InfluxDB database server - `port`: Portnumber (as string) of the InfluxDB database server
- `user`: Username for basic authentification - `user`: Username for basic authentification
@ -33,7 +32,7 @@ The `influxdb` sink uses the official [InfluxDB golang client](https://pkg.go.de
- `organization`: Organization in the InfluxDB - `organization`: Organization in the InfluxDB
- `ssl`: Use SSL connection - `ssl`: Use SSL connection
- `flush_delay`: Group metrics coming in to a single batch - `flush_delay`: Group metrics coming in to a single batch
- `batch_size`: Maximal batch size - `batch_size`: Maximal batch size. If `batch_size` is reached before the end of `flush_delay`, the metrics are sent without further delay
Influx client options: Influx client options: