mirror of
https://github.com/mainflux/mainflux.git
synced 2025-05-02 22:17:10 +08:00

* Update dependencies Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * Update dependencies Fix Timescale Reader bug. Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * Revert influxdb-reader changes Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> * Update dependencies to latest supported versions Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> --------- Signed-off-by: dusanb94 <dusan.borovcanin@mainflux.com> Co-authored-by: Drasko DRASKOVIC <drasko.draskovic@gmail.com>
304 lines
7.2 KiB
Go
304 lines
7.2 KiB
Go
package protocol
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// ErrIsNaN is a field error for when a float field is NaN.
|
|
var ErrIsNaN = &FieldError{"is NaN"}
|
|
|
|
// ErrIsInf is a field error for when a float field is Inf.
|
|
var ErrIsInf = &FieldError{"is Inf"}
|
|
|
|
// Encoder marshals Metrics into influxdb line protocol.
|
|
// It is not safe for concurrent use, make a new one!
|
|
// The default behavior when encountering a field error is to ignore the field and move on.
|
|
// If you wish it to error out on field errors, use Encoder.FailOnFieldErr(true)
|
|
type Encoder struct {
|
|
w io.Writer
|
|
fieldSortOrder FieldSortOrder
|
|
fieldTypeSupport FieldTypeSupport
|
|
failOnFieldError bool
|
|
maxLineBytes int
|
|
fieldList []*Field
|
|
header []byte
|
|
footer []byte
|
|
pair []byte
|
|
precision time.Duration
|
|
}
|
|
|
|
// SetMaxLineBytes sets a maximum length for a line, Encode will error if the generated line is longer
|
|
func (e *Encoder) SetMaxLineBytes(i int) {
|
|
e.maxLineBytes = i
|
|
}
|
|
|
|
// SetFieldSortOrder sets a sort order for the data.
|
|
// The options are:
|
|
// NoSortFields (doesn't sort the fields)
|
|
// SortFields (sorts the keys in alphabetical order)
|
|
func (e *Encoder) SetFieldSortOrder(s FieldSortOrder) {
|
|
e.fieldSortOrder = s
|
|
}
|
|
|
|
// SetFieldTypeSupport sets flags for if the encoder supports certain optional field types such as uint64
|
|
func (e *Encoder) SetFieldTypeSupport(s FieldTypeSupport) {
|
|
e.fieldTypeSupport = s
|
|
}
|
|
|
|
// FailOnFieldErr whether or not to fail on a field error or just move on.
|
|
// The default behavior to move on
|
|
func (e *Encoder) FailOnFieldErr(s bool) {
|
|
e.failOnFieldError = s
|
|
}
|
|
|
|
// SetPrecision sets time precision for writes
|
|
// Default is nanoseconds precision
|
|
func (e *Encoder) SetPrecision(p time.Duration) {
|
|
e.precision = p
|
|
}
|
|
|
|
// NewEncoder gives us an encoder that marshals to a writer in influxdb line protocol
|
|
// as defined by:
|
|
// https://docs.influxdata.com/influxdb/v1.5/write_protocols/line_protocol_reference/
|
|
func NewEncoder(w io.Writer) *Encoder {
|
|
return &Encoder{
|
|
w: w,
|
|
header: make([]byte, 0, 128),
|
|
footer: make([]byte, 0, 128),
|
|
pair: make([]byte, 0, 128),
|
|
fieldList: make([]*Field, 0, 16),
|
|
precision: time.Nanosecond,
|
|
}
|
|
}
|
|
|
|
// This is here to significantly reduce allocations, wish that we had constant/immutable keyword that applied to
|
|
// more complex objects
|
|
var comma = []byte(",")
|
|
|
|
// Encode marshals a Metric to the io.Writer in the Encoder
|
|
func (e *Encoder) Encode(m Metric) (int, error) {
|
|
err := e.buildHeader(m)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
e.buildFooter(m.Time())
|
|
|
|
// here we make a copy of the *fields so we can do an in-place sort
|
|
e.fieldList = append(e.fieldList[:0], m.FieldList()...)
|
|
|
|
if e.fieldSortOrder == SortFields {
|
|
sort.Slice(e.fieldList, func(i, j int) bool {
|
|
return e.fieldList[i].Key < e.fieldList[j].Key
|
|
})
|
|
}
|
|
i := 0
|
|
totalWritten := 0
|
|
pairsLen := 0
|
|
firstField := true
|
|
for _, field := range e.fieldList {
|
|
err = e.buildFieldPair(field.Key, field.Value)
|
|
if err != nil {
|
|
if e.failOnFieldError {
|
|
return 0, err
|
|
}
|
|
continue
|
|
}
|
|
|
|
bytesNeeded := len(e.header) + pairsLen + len(e.pair) + len(e.footer)
|
|
|
|
// Additional length needed for field separator `,`
|
|
if !firstField {
|
|
bytesNeeded++
|
|
}
|
|
|
|
if e.maxLineBytes > 0 && bytesNeeded > e.maxLineBytes {
|
|
// Need at least one field per line
|
|
if firstField {
|
|
return 0, ErrNeedMoreSpace
|
|
}
|
|
|
|
i, err = e.w.Write(e.footer)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
pairsLen = 0
|
|
totalWritten += i
|
|
|
|
bytesNeeded = len(e.header) + len(e.pair) + len(e.footer)
|
|
|
|
if e.maxLineBytes > 0 && bytesNeeded > e.maxLineBytes {
|
|
return 0, ErrNeedMoreSpace
|
|
}
|
|
|
|
i, err = e.w.Write(e.header)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalWritten += i
|
|
|
|
i, err = e.w.Write(e.pair)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalWritten += i
|
|
|
|
pairsLen += len(e.pair)
|
|
firstField = false
|
|
continue
|
|
}
|
|
|
|
if firstField {
|
|
i, err = e.w.Write(e.header)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalWritten += i
|
|
|
|
} else {
|
|
i, err = e.w.Write(comma)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalWritten += i
|
|
|
|
}
|
|
|
|
i, err = e.w.Write(e.pair)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalWritten += i
|
|
|
|
pairsLen += len(e.pair)
|
|
firstField = false
|
|
}
|
|
|
|
if firstField {
|
|
return 0, ErrNoFields
|
|
}
|
|
i, err = e.w.Write(e.footer)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalWritten += i
|
|
return totalWritten, nil
|
|
|
|
}
|
|
|
|
func (e *Encoder) buildHeader(m Metric) error {
|
|
e.header = e.header[:0]
|
|
name := nameEscape(m.Name())
|
|
if name == "" {
|
|
return ErrInvalidName
|
|
}
|
|
e.header = append(e.header, name...)
|
|
|
|
for _, tag := range m.TagList() {
|
|
key := escape(tag.Key)
|
|
value := escape(tag.Value)
|
|
|
|
// Some keys and values are not encodeable as line protocol, such as
|
|
// those with a trailing '\' or empty strings.
|
|
if key == "" || value == "" {
|
|
continue
|
|
}
|
|
|
|
e.header = append(e.header, ',')
|
|
e.header = append(e.header, key...)
|
|
e.header = append(e.header, '=')
|
|
e.header = append(e.header, value...)
|
|
}
|
|
|
|
e.header = append(e.header, ' ')
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) buildFieldVal(value interface{}) error {
|
|
switch v := value.(type) {
|
|
case uint64:
|
|
if e.fieldTypeSupport&UintSupport != 0 {
|
|
e.pair = append(strconv.AppendUint(e.pair, v, 10), 'u')
|
|
} else if v <= uint64(math.MaxInt64) {
|
|
e.pair = append(strconv.AppendInt(e.pair, int64(v), 10), 'i')
|
|
} else {
|
|
e.pair = append(strconv.AppendInt(e.pair, math.MaxInt64, 10), 'i')
|
|
}
|
|
case int64:
|
|
e.pair = append(strconv.AppendInt(e.pair, v, 10), 'i')
|
|
case int:
|
|
e.pair = append(strconv.AppendInt(e.pair, int64(v), 10), 'i')
|
|
case float64:
|
|
if math.IsNaN(v) {
|
|
return ErrIsNaN
|
|
}
|
|
|
|
if math.IsInf(v, 0) {
|
|
return ErrIsInf
|
|
}
|
|
|
|
e.pair = strconv.AppendFloat(e.pair, v, 'f', -1, 64)
|
|
case float32:
|
|
v32 := float64(v)
|
|
if math.IsNaN(v32) {
|
|
return ErrIsNaN
|
|
}
|
|
|
|
if math.IsInf(v32, 0) {
|
|
return ErrIsInf
|
|
}
|
|
|
|
e.pair = strconv.AppendFloat(e.pair, v32, 'f', -1, 64)
|
|
|
|
case string:
|
|
e.pair = append(e.pair, '"')
|
|
e.pair = append(e.pair, stringFieldEscape(v)...)
|
|
e.pair = append(e.pair, '"')
|
|
case []byte:
|
|
e.pair = append(e.pair, '"')
|
|
stringFieldEscapeBytes(&e.pair, v)
|
|
e.pair = append(e.pair, '"')
|
|
case bool:
|
|
e.pair = strconv.AppendBool(e.pair, v)
|
|
default:
|
|
return &FieldError{fmt.Sprintf("invalid value type: %T", v)}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Encoder) buildFieldPair(key string, value interface{}) error {
|
|
e.pair = e.pair[:0]
|
|
key = escape(key)
|
|
// Some keys are not encodeable as line protocol, such as those with a
|
|
// trailing '\' or empty strings.
|
|
if key == "" || key[:len(key)-1] == "\\" {
|
|
return &FieldError{"invalid field key"}
|
|
}
|
|
e.pair = append(e.pair, key...)
|
|
e.pair = append(e.pair, '=')
|
|
return e.buildFieldVal(value)
|
|
}
|
|
|
|
func (e *Encoder) buildFooter(t time.Time) {
|
|
e.footer = e.footer[:0]
|
|
if !t.IsZero() {
|
|
e.footer = append(e.footer, ' ')
|
|
switch e.precision {
|
|
case time.Microsecond:
|
|
e.footer = strconv.AppendInt(e.footer, t.UnixNano()/1000, 10)
|
|
case time.Millisecond:
|
|
e.footer = strconv.AppendInt(e.footer, t.UnixNano()/1000000, 10)
|
|
case time.Second:
|
|
e.footer = strconv.AppendInt(e.footer, t.Unix(), 10)
|
|
default:
|
|
e.footer = strconv.AppendInt(e.footer, t.UnixNano(), 10)
|
|
}
|
|
}
|
|
e.footer = append(e.footer, '\n')
|
|
}
|