Skip to content

Commit 8b17613

Browse files
authored
Redis latency (#888)
Store basic key/value fields in redis for accessing ksynth latency.
1 parent 4c7af5a commit 8b17613

6 files changed

Lines changed: 290 additions & 0 deletions

File tree

cmd/ktranslate/main.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,27 @@ func applyFlags(cfg *ktranslate.Config) error {
463463
return
464464
}
465465
cfg.OtelFormat.NoBlockExport = v
466+
// pkg/formats/redis
467+
case "redis.addr":
468+
cfg.RedisFormat.RedisAddr = val
469+
case "redis.password":
470+
cfg.RedisFormat.RedisPassword = val
471+
case "redis.db":
472+
v, err := strconv.Atoi(val)
473+
if err != nil {
474+
errCh <- err
475+
return
476+
}
477+
cfg.RedisFormat.RedisDB = v
478+
case "redis.ttl.sec":
479+
v, err := strconv.Atoi(val)
480+
if err != nil {
481+
errCh <- err
482+
return
483+
}
484+
cfg.RedisFormat.KeyTTLSeconds = v
485+
case "redis.key_prefix":
486+
cfg.RedisFormat.KeyPrefix = val
466487
// pkg/formats/elasticsearch
467488
case "elastic.action":
468489
cfg.ElasticFormat.Action = val

config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ type OtelFormatConfig struct {
5252
NoBlockExport bool
5353
}
5454

55+
// RedisFormatConfig is the config for the redis format
56+
type RedisFormatConfig struct {
57+
RedisAddr string
58+
RedisPassword string
59+
RedisDB int
60+
KeyPrefix string
61+
KeyTTLSeconds int
62+
}
63+
5564
// SnmpFormatConfig is the config for the snmp format
5665
type SnmpFormatConfig struct {
5766
ConfigFile string
@@ -329,6 +338,8 @@ type Config struct {
329338
InfluxDBFormat *InfluxDBFormatConfig
330339
// pkg/formats/otel
331340
OtelFormat *OtelFormatConfig
341+
// pkg/formats/redis
342+
RedisFormat *RedisFormatConfig
332343
// pkg/formats/snmp
333344
SnmpFormat *SnmpFormatConfig
334345
// pkg/formats/elasticsearch
@@ -432,6 +443,13 @@ func DefaultConfig() *Config {
432443
RootCA: "",
433444
NoBlockExport: false,
434445
},
446+
RedisFormat: &RedisFormatConfig{
447+
RedisAddr: "localhost:6379",
448+
RedisPassword: "",
449+
RedisDB: 0,
450+
KeyPrefix: "",
451+
KeyTTLSeconds: 60,
452+
},
435453
ElasticFormat: &ElasticFormatConfig{
436454
Action: "index",
437455
},

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ require (
4545
github.com/prometheus-community/pro-bing v0.3.0
4646
github.com/prometheus/client_golang v1.19.0
4747
github.com/prometheus/prometheus v0.46.0
48+
github.com/redis/go-redis/v9 v9.18.0
4849
github.com/sasha-s/go-hll v0.0.0-20180522065212-c6eb27aee351
4950
github.com/stretchr/testify v1.11.1
5051
github.com/syndtr/goleveldb v1.0.0
@@ -90,6 +91,7 @@ require (
9091
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
9192
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
9293
github.com/dgryski/go-bits v0.0.0-20180113010104-bd8a69a71dc2 // indirect
94+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
9395
github.com/eapache/go-resiliency v1.3.0 // indirect
9496
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 // indirect
9597
github.com/eapache/queue v1.1.0 // indirect
@@ -165,6 +167,7 @@ require (
165167
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
166168
go.opentelemetry.io/otel/trace v1.43.0 // indirect
167169
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
170+
go.uber.org/atomic v1.11.0 // indirect
168171
golang.org/x/crypto v0.49.0 // indirect
169172
golang.org/x/oauth2 v0.35.0 // indirect
170173
golang.org/x/sync v0.20.0 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
228228
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
229229
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
230230
github.com/bobg/gcsobj v0.1.2/go.mod h1:vS49EQ1A1Ib8FgrL58C8xXYZyOCR2TgzAdopy6/ipa8=
231+
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
232+
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
233+
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
234+
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
231235
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
232236
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
233237
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -279,6 +283,8 @@ github.com/denisenkom/go-mssqldb v0.12.0/go.mod h1:iiK0YP1ZeepvmBQk/QpLEhhTNJgfz
279283
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
280284
github.com/dgryski/go-bits v0.0.0-20180113010104-bd8a69a71dc2 h1:2+yip7nN/auel0PDwY7SIaTOxQPI2NwdkZkvpgtc3Pk=
281285
github.com/dgryski/go-bits v0.0.0-20180113010104-bd8a69a71dc2/go.mod h1:/9UYwwvZuEgp+mQ4960SHWCU1FS+FgdFX+m5ExFByNs=
286+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
287+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
282288
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
283289
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
284290
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
@@ -824,6 +830,8 @@ github.com/prometheus/prometheus v0.46.0 h1:9JSdXnsuT6YsbODEhSQMwxNkGwPExfmzqG73
824830
github.com/prometheus/prometheus v0.46.0/go.mod h1:10L5IJE5CEsjee1FnOcVswYXlPIscDWWt3IJ2UDYrz4=
825831
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
826832
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
833+
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
834+
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
827835
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
828836
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
829837
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -914,6 +922,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
914922
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
915923
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
916924
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
925+
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
926+
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
917927
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
918928
go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8=
919929
go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M=
@@ -965,6 +975,8 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
965975
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
966976
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
967977
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
978+
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
979+
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
968980
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
969981
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
970982
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=

pkg/formats/format.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/kentik/ktranslate/pkg/formats/otel"
2020
"github.com/kentik/ktranslate/pkg/formats/parquet"
2121
"github.com/kentik/ktranslate/pkg/formats/prom"
22+
"github.com/kentik/ktranslate/pkg/formats/redis"
2223
"github.com/kentik/ktranslate/pkg/formats/snmp"
2324
"github.com/kentik/ktranslate/pkg/formats/splunk"
2425
"github.com/kentik/ktranslate/pkg/kt"
@@ -53,6 +54,7 @@ const (
5354
FORMAT_OTEL = "otel"
5455
FORMAT_SNMP = "snmp"
5556
FORMAT_PARQUET = "parquet"
57+
FORMAT_REDIS = "redis"
5658
)
5759

5860
func NewFormat(ctx context.Context, format Format, log logger.Underlying, registry go_metrics.Registry, compression kt.Compression, cfg *ktranslate.Config, logTee chan string) (Formatter, error) {
@@ -89,6 +91,8 @@ func NewFormat(ctx context.Context, format Format, log logger.Underlying, regist
8991
return snmp.NewFormat(log, cfg.SnmpFormat)
9092
case FORMAT_PARQUET:
9193
return parquet.NewFormat(log, compression)
94+
case FORMAT_REDIS:
95+
return redis.NewFormat(ctx, log, cfg.RedisFormat)
9296
default:
9397
return nil, fmt.Errorf("You used an unsupported format: %v.", format)
9498
}

pkg/formats/redis/redis.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package redis
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"net"
8+
"sync"
9+
"time"
10+
11+
"github.com/kentik/ktranslate"
12+
"github.com/kentik/ktranslate/pkg/eggs/logger"
13+
"github.com/kentik/ktranslate/pkg/formats/util"
14+
"github.com/kentik/ktranslate/pkg/kt"
15+
"github.com/kentik/ktranslate/pkg/rollup"
16+
17+
"github.com/redis/go-redis/v9"
18+
)
19+
20+
type RedisFormat struct {
21+
logger.ContextL
22+
lastMetadata map[string]*kt.LastMetadata
23+
mux sync.RWMutex
24+
config *ktranslate.RedisFormatConfig
25+
ctx context.Context
26+
invalids map[string]bool
27+
rdb *redis.Client
28+
keyTTL time.Duration
29+
}
30+
31+
var (
32+
redisAddr string
33+
redisPassword string
34+
redisDB int
35+
keyPrefix string
36+
keyTTLSec int
37+
)
38+
39+
const (
40+
aPrefix = ":A"
41+
aaaaPrefix = ":AAAA"
42+
)
43+
44+
func init() {
45+
flag.StringVar(&redisAddr, "redis.addr", "localhost:6379", "Where to connect to redis.")
46+
flag.StringVar(&redisPassword, "redis.password", "", "Password for redis")
47+
flag.IntVar(&redisDB, "redis.db", 0, "Use this redis DB.")
48+
flag.StringVar(&keyPrefix, "redis.key_prefix", "", "Use this key prefix.")
49+
flag.IntVar(&keyTTLSec, "redis.ttl.sec", 60, "Expire measurements if they are not refreshed within this number of sec.")
50+
}
51+
52+
func NewFormat(ctx context.Context, log logger.Underlying, cfg *ktranslate.RedisFormatConfig) (*RedisFormat, error) {
53+
jf := &RedisFormat{
54+
ContextL: logger.NewContextLFromUnderlying(logger.SContext{S: "redis"}, log),
55+
lastMetadata: map[string]*kt.LastMetadata{},
56+
invalids: map[string]bool{},
57+
keyTTL: time.Second * time.Duration(cfg.KeyTTLSeconds),
58+
ctx: ctx,
59+
config: cfg,
60+
}
61+
62+
jf.rdb = redis.NewClient(&redis.Options{
63+
Addr: cfg.RedisAddr,
64+
Password: cfg.RedisPassword,
65+
DB: cfg.RedisDB,
66+
})
67+
68+
if err := jf.rdb.Ping(ctx).Err(); err != nil {
69+
return nil, fmt.Errorf("cannot connect to Redis addr %s err %v", cfg.RedisAddr, err)
70+
}
71+
72+
jf.Infof("connected to Redis: addr=%s, ttl: %v", cfg.RedisAddr, jf.keyTTL)
73+
74+
return jf, nil
75+
}
76+
77+
func (f *RedisFormat) To(msgs []*kt.JCHF, serBuf []byte) (*kt.Output, error) {
78+
res := map[string][]RedisData{}
79+
for _, m := range msgs {
80+
for fqdn, r := range f.toRedisMetric(m) {
81+
if _, ok := res[fqdn]; !ok {
82+
res[fqdn] = r
83+
} else {
84+
res[fqdn] = append(res[fqdn], r...)
85+
}
86+
}
87+
}
88+
89+
if len(res) == 0 {
90+
return nil, nil
91+
}
92+
93+
pipe := f.rdb.Pipeline()
94+
95+
for fqdn, results := range res {
96+
key := f.config.KeyPrefix + fqdn
97+
aKey := key + aPrefix
98+
aaaaKey := key + aaaaPrefix
99+
100+
aValues := make(map[string]interface{})
101+
aaaaValues := make(map[string]interface{})
102+
aFields := make([]string, 0, len(results))
103+
aaaaFields := make([]string, 0, len(results))
104+
aSeen := make(map[string]struct{})
105+
aaaaSeen := make(map[string]struct{})
106+
107+
for _, r := range results {
108+
field := r.IP.String()
109+
if r.Is6 {
110+
aaaaValues[field] = r.Latency
111+
if _, ok := aaaaSeen[field]; !ok {
112+
aaaaSeen[field] = struct{}{}
113+
aaaaFields = append(aaaaFields, field)
114+
}
115+
} else {
116+
aValues[field] = r.Latency
117+
if _, ok := aSeen[field]; !ok {
118+
aSeen[field] = struct{}{}
119+
aFields = append(aFields, field)
120+
}
121+
}
122+
}
123+
124+
if len(aValues) > 0 {
125+
pipe.HSet(f.ctx, aKey, aValues)
126+
pipe.HExpire(f.ctx, aKey, f.keyTTL, aFields...)
127+
}
128+
if len(aaaaValues) > 0 {
129+
pipe.HSet(f.ctx, aaaaKey, aaaaValues)
130+
pipe.HExpire(f.ctx, aaaaKey, f.keyTTL, aaaaFields...)
131+
}
132+
f.Debugf("queued HSet key %s members %d", key, len(results))
133+
}
134+
135+
_, err := pipe.Exec(f.ctx)
136+
return nil, err
137+
}
138+
139+
func (f *RedisFormat) From(raw *kt.Output) ([]map[string]interface{}, error) {
140+
values := make([]map[string]interface{}, 0)
141+
return values, nil
142+
}
143+
144+
func (f *RedisFormat) Rollup(rolls []rollup.Rollup) (*kt.Output, error) {
145+
return nil, nil
146+
}
147+
148+
func (f *RedisFormat) toRedisMetric(in *kt.JCHF) map[string][]RedisData {
149+
switch in.EventType {
150+
case kt.KENTIK_EVENT_SYNTH:
151+
return f.fromKSynth(in)
152+
case kt.KENTIK_EVENT_SNMP_METADATA:
153+
return f.fromSnmpMetadata(in)
154+
default:
155+
f.mux.Lock()
156+
defer f.mux.Unlock()
157+
if !f.invalids[in.EventType] {
158+
f.Warnf("Invalid EventType: %s", in.EventType)
159+
f.invalids[in.EventType] = true
160+
}
161+
}
162+
163+
return nil
164+
}
165+
166+
func (f *RedisFormat) fromKSynth(in *kt.JCHF) map[string][]RedisData {
167+
if in.CustomInt["result_type"] <= 1 {
168+
return nil // Don't worry about timeouts and errors for now.
169+
}
170+
171+
metrics := util.GetSynMetricNameSet(in.CustomInt["result_type"])
172+
attr := map[string]interface{}{}
173+
f.mux.RLock()
174+
util.SetAttr(attr, in, metrics, f.lastMetadata[in.DeviceName], false)
175+
f.mux.RUnlock()
176+
ms := make([]RedisData, 0, len(metrics))
177+
var fqdn string
178+
var ip net.IP
179+
180+
if tn, ok := attr["test_name"].(string); ok {
181+
fqdn = tn
182+
} else {
183+
return nil
184+
}
185+
if da, ok := attr["dst_addr"].(string); ok {
186+
ip = net.ParseIP(da)
187+
if ip == nil {
188+
return nil
189+
}
190+
} else {
191+
return nil
192+
}
193+
194+
for m, name := range metrics {
195+
switch name.Name {
196+
case "avg_rtt":
197+
ms = append(ms, RedisData{
198+
IP: ip,
199+
Latency: float64(in.CustomInt[m]),
200+
Is6: (ip.To4() == nil),
201+
})
202+
}
203+
}
204+
205+
return map[string][]RedisData{fqdn: ms}
206+
}
207+
208+
func (f *RedisFormat) fromSnmpMetadata(in *kt.JCHF) map[string][]RedisData {
209+
if in.DeviceName == "" { // Only run if this is set.
210+
return nil
211+
}
212+
213+
lm := util.SetMetadata(in)
214+
215+
f.mux.Lock()
216+
defer f.mux.Unlock()
217+
if f.lastMetadata[in.DeviceName] == nil || lm.Size() >= f.lastMetadata[in.DeviceName].Size() {
218+
f.Infof("New Metadata for %s", in.DeviceName)
219+
f.lastMetadata[in.DeviceName] = lm
220+
} else {
221+
f.Infof("The metadata for %s was not updated since the attribute size is smaller. New = %d < Old = %d, Size difference = %v.",
222+
in.DeviceName, lm.Size(), f.lastMetadata[in.DeviceName].Size(), f.lastMetadata[in.DeviceName].Missing(lm))
223+
}
224+
225+
return nil
226+
}
227+
228+
type RedisData struct {
229+
IP net.IP
230+
Latency float64
231+
Is6 bool
232+
}

0 commit comments

Comments
 (0)