All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Add an optional exporter that writes one point per proxied request to the InfluxDB 3 write endpoint. Points are batched in memory and flushed by size or interval. The exporter is off until GATEWAY_INFLUX_URL is set.
92 lines
2.3 KiB
Go
92 lines
2.3 KiB
Go
package influx
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Point is one line-protocol row.
|
|
type Point struct {
|
|
Measurement string
|
|
Tags map[string]string
|
|
Fields map[string]any
|
|
Time time.Time
|
|
}
|
|
|
|
// encode appends the point as one line-protocol line. It returns false when
|
|
// the point has no usable field, because InfluxDB rejects such a line.
|
|
func (p Point) encode(b *strings.Builder) bool {
|
|
fields := make([]string, 0, len(p.Fields))
|
|
for _, k := range sortedKeys(p.Fields) {
|
|
v, ok := formatField(p.Fields[k])
|
|
if !ok {
|
|
continue
|
|
}
|
|
fields = append(fields, escapeKey(k)+"="+v)
|
|
}
|
|
if len(fields) == 0 {
|
|
return false
|
|
}
|
|
|
|
b.WriteString(escapeMeasurement(p.Measurement))
|
|
for _, k := range sortedKeys(p.Tags) {
|
|
v := p.Tags[k]
|
|
if v == "" {
|
|
continue
|
|
}
|
|
b.WriteByte(',')
|
|
b.WriteString(escapeKey(k))
|
|
b.WriteByte('=')
|
|
b.WriteString(escapeKey(v))
|
|
}
|
|
b.WriteByte(' ')
|
|
b.WriteString(strings.Join(fields, ","))
|
|
b.WriteByte(' ')
|
|
b.WriteString(strconv.FormatInt(p.Time.UnixNano(), 10))
|
|
b.WriteByte('\n')
|
|
return true
|
|
}
|
|
|
|
// formatField renders a field value. Unsupported types and non-finite floats
|
|
// are dropped so one bad value does not reject the whole batch.
|
|
func formatField(v any) (string, bool) {
|
|
switch x := v.(type) {
|
|
case int:
|
|
return strconv.FormatInt(int64(x), 10) + "i", true
|
|
case int64:
|
|
return strconv.FormatInt(x, 10) + "i", true
|
|
case float64:
|
|
if math.IsNaN(x) || math.IsInf(x, 0) {
|
|
return "", false
|
|
}
|
|
return strconv.FormatFloat(x, 'f', -1, 64), true
|
|
case bool:
|
|
return strconv.FormatBool(x), true
|
|
case string:
|
|
return `"` + escapeString(x) + `"`, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func sortedKeys[V any](m map[string]V) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
var (
|
|
measurementEscaper = strings.NewReplacer(",", `\,`, " ", `\ `, "\n", " ")
|
|
keyEscaper = strings.NewReplacer(",", `\,`, "=", `\=`, " ", `\ `, "\n", " ")
|
|
stringEscaper = strings.NewReplacer(`\`, `\\`, `"`, `\"`, "\n", " ", "\r", " ")
|
|
)
|
|
|
|
func escapeMeasurement(s string) string { return measurementEscaper.Replace(s) }
|
|
func escapeKey(s string) string { return keyEscaper.Replace(s) }
|
|
func escapeString(s string) string { return stringEscaper.Replace(s) }
|