Files
localmodelroutex/backend/internal/influx/lineprotocol.go
LovroG05 abecc4b888
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: export request statistics to InfluxDB 3
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.
2026-10-11 17:20:14 +02:00

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) }