Skip to content

Commit 7ee7c9c

Browse files
committed
Add Redis pub/sub datastore
- Add a Redis dispatcher that publishes telemetry records over Redis pub/sub, selectable per record type via the "redis" dispatch rule. - Route each record to the per-VIN subscriber channels in a sorted set (configurable prefix); entries scored with an epoch lease expiry are purged when past due. With publish_vin_topics, also publish to the VIN channel <namespace>_<txtype>_{<vin>}.
1 parent 20df968 commit 7ee7c9c

12 files changed

Lines changed: 637 additions & 38 deletions

File tree

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ For ease of installation and operation, run Fleet Telemetry on Kubernetes or a s
8181
"V": "custom_stream_name"
8282
}
8383
},
84+
"redis": { // Redis pub/sub config
85+
"addrs": ["redis:6379"], // one or more addresses (cluster/sentinel supported)
86+
"username": string - optional,
87+
"password": string - optional,
88+
"db": int - optional,
89+
"subscriber_set_prefix": string - optional; names the per-VIN subscriber sorted set. When empty, the sorted set is not consulted,
90+
"publish_vin_topics": bool - additionally publish each record to the VIN channel namespace_topic_{vin},
91+
"tls": { "ca_file": string, "server_cert": string, "server_key": string }, // optional
92+
"pool": { "pool_size": int, "min_idle_conns": int, "conn_max_lifetime": int } // optional
93+
},
8494
"rate_limit": {
8595
"enabled": bool,
8696
"message_limit": int - ex.: 1000
@@ -149,7 +159,7 @@ spec:
149159
Vehicles must be running firmware version 2023.20.6 or later. Some older model S/X are not supported.
150160
151161
## Personalized Backends/Dispatchers
152-
Dispatchers handle vehicle data processing upon its arrival at Fleet Telemetry servers. They can be of any type, from distributed message queues to STDOUT logger. Here is a list of the currently supported [dispatchers](./telemetry/producer.go#L10-L19)::
162+
Dispatchers handle vehicle data processing upon its arrival at Fleet Telemetry servers. They can be of any type, from distributed message queues to STDOUT logger. Here is a list of the currently supported [dispatchers](./telemetry/producer.go#L13-L26)::
153163
* Kafka (preferred): Configure with the config.json file. See implementation here: [config/config.go](./config/config.go)
154164
* Topics will need to be created for \*prefix\*`_V`,\*prefix\*`_connectivity` and \*prefix\*`_alerts`. The default prefix is `tesla`
155165
* Kinesis: Configure with standard [AWS env variables and config files](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html). The default AWS credentials and config files are: `~/.aws/credentials` and `~/.aws/config`.
@@ -161,12 +171,17 @@ Dispatchers handle vehicle data processing upon its arrival at Fleet Telemetry s
161171
* ZMQ: Configure with the config.json file. See implementation here: [config/config.go](./config/config.go)
162172
* MQTT: Configure using the config.json file. See implementation in [config/config.go](./config/config.go)
163173
* See detailed MQTT information in the [MQTT README](./datastore/mqtt/README.md)
174+
* Redis: Publishes records to Redis Pub/Sub channels. Configure with the config.json file. See implementation here: [datastore/redis/redis.go](./datastore/redis/redis.go)
175+
* When `subscriber_set_prefix` is set, the producer reads a per-VIN sorted set at `<subscriber_set_prefix>_`\*namespace\*`_`\*topic\*`_{`\*vin\*`}` (e.g. `consumer_tesla_V_{<vin>}`). Sorted-set scores are subscriber lease expiries (epoch seconds); entries scored below the current time are purged before each publish, and the payload is published to every surviving member channel.
176+
* If `publish_vin_topics` is enabled, the payload is additionally published to the VIN channel \*namespace\*`_`\*topic\*`_{`\*vin\*`}` (e.g. `tesla_V_{<vin>}`). Member channels and the VIN channel both receive the record.
177+
* At least one of `subscriber_set_prefix` or `publish_vin_topics` must be configured — otherwise no records could ever be published and the server fails to start.
178+
* Supports TLS via the `tls` block and connection-pool tuning via the `pool` block.
164179
* Logger: This is a simple STDOUT logger that serializes the protos to json.
165180

166181
>NOTE: To add a new dispatcher, please provide integration tests and updated documentation. To serialize dispatcher data as json instead of protobufs, add a config `transmit_decoded_records` and set value to `true` as shown [here](config/test_configs_test.go#L186)
167182

168183
## Reliable Acks
169-
Fleet Telemetry can send ack messages back to the vehicle. This is useful for applications that need to ensure the data was received and processed. To enable this feature, set `reliable_ack_sources` to one of configured dispatchers (`kafka`,`kinesis`,`pubsub`,`zmq`, `mqtt`) in the config file. Reliable acks can only be set to one dispatcher per recordType. See [here](./test/integration/config.json#L8) for sample config.
184+
Fleet Telemetry can send ack messages back to the vehicle. This is useful for applications that need to ensure the data was received and processed. To enable this feature, set `reliable_ack_sources` to one of configured dispatchers (`kafka`,`kinesis`,`pubsub`,`zmq`, `mqtt`, `redis`) in the config file. Reliable acks can only be set to one dispatcher per recordType. See [here](./test/integration/config.json#L8) for sample config.
170185

171186
## Detecting Vehicle Connectivity Changes
172187
On the vehicle, Fleet Telemetry client behave similarly to how the connectivity engine for vehicle commands. Therefore we can use Fleet Telemetry connectivity event to assume when a vehicle is online. Note that it is a proxy, but if configured properly Fleet Telemetry connectivity time should match vehicle connectivity state in 99%+. To enable connectivity events simply add the `connectivity` records in the list of events in [server_config.json](./examples/server_config.json) file:

config/config.go

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import (
1616
githubairbrake "github.com/airbrake/gobrake/v5"
1717

1818
confluent "github.com/confluentinc/confluent-kafka-go/v2/kafka"
19+
goredis "github.com/redis/go-redis/v9"
1920
githublogrus "github.com/sirupsen/logrus"
2021

2122
"github.com/teslamotors/fleet-telemetry/datastore/googlepubsub"
2223
"github.com/teslamotors/fleet-telemetry/datastore/kafka"
2324
"github.com/teslamotors/fleet-telemetry/datastore/kinesis"
2425
"github.com/teslamotors/fleet-telemetry/datastore/mqtt"
26+
redisdatastore "github.com/teslamotors/fleet-telemetry/datastore/redis"
2527
"github.com/teslamotors/fleet-telemetry/datastore/simple"
2628
"github.com/teslamotors/fleet-telemetry/datastore/zmq"
2729
logrus "github.com/teslamotors/fleet-telemetry/logger"
@@ -71,6 +73,9 @@ type Config struct {
7173
// ZMQ configures a zeromq socket
7274
ZMQ *zmq.Config `json:"zmq,omitempty"`
7375

76+
// Redis configures a Redis pub/sub producer
77+
Redis *Redis `json:"redis,omitempty"`
78+
7479
// Namespace defines a prefix for the kafka/pubsub topic
7580
Namespace string `json:"namespace,omitempty"`
7681

@@ -148,6 +153,60 @@ type Kinesis struct {
148153
Streams map[string]string `json:"streams,omitempty"`
149154
}
150155

156+
// Redis is a configuration for the Redis pub/sub producer.
157+
type Redis struct {
158+
Addrs []string `json:"addrs"`
159+
Username string `json:"username,omitempty"`
160+
Password string `json:"password,omitempty"`
161+
DB int `json:"db,omitempty"`
162+
TLS *TLS `json:"tls,omitempty"`
163+
Pool *RedisPool `json:"pool,omitempty"`
164+
165+
// PublishVINTopics, when true, falls back to publishing on the VIN set-key
166+
// channel when its subscriber sorted set is empty.
167+
PublishVINTopics bool `json:"publish_vin_topics,omitempty"`
168+
169+
// SubscriberSetPrefix names the per-VIN sorted set of subscriber channels
170+
// (<prefix>_<namespace>_<txtype>_{<vin>}). When empty, the sorted set is not
171+
// consulted and records route only via the VIN fallback channel.
172+
SubscriberSetPrefix string `json:"subscriber_set_prefix,omitempty"`
173+
}
174+
175+
// RedisPool configures the Redis connection pool. Zero values use go-redis defaults.
176+
type RedisPool struct {
177+
PoolSize int `json:"pool_size,omitempty"`
178+
MinIdleConns int `json:"min_idle_conns,omitempty"`
179+
MaxIdleConns int `json:"max_idle_conns,omitempty"`
180+
MaxActiveConns int `json:"max_active_conns,omitempty"`
181+
PoolTimeout time.Duration `json:"pool_timeout,omitempty"`
182+
ConnMaxIdleTime time.Duration `json:"conn_max_idle_time,omitempty"`
183+
ConnMaxLifetime time.Duration `json:"conn_max_lifetime,omitempty"`
184+
}
185+
186+
func (r *Redis) options() (*goredis.UniversalOptions, error) {
187+
tlsConfig, err := r.TLS.ClientTLSConfig()
188+
if err != nil {
189+
return nil, err
190+
}
191+
options := &goredis.UniversalOptions{
192+
Addrs: r.Addrs,
193+
Username: r.Username,
194+
Password: r.Password,
195+
DB: r.DB,
196+
TLSConfig: tlsConfig,
197+
}
198+
if r.Pool != nil {
199+
options.PoolSize = r.Pool.PoolSize
200+
options.MinIdleConns = r.Pool.MinIdleConns
201+
options.MaxIdleConns = r.Pool.MaxIdleConns
202+
options.MaxActiveConns = r.Pool.MaxActiveConns
203+
options.PoolTimeout = r.Pool.PoolTimeout
204+
options.ConnMaxIdleTime = r.Pool.ConnMaxIdleTime
205+
options.ConnMaxLifetime = r.Pool.ConnMaxLifetime
206+
}
207+
return options, nil
208+
}
209+
151210
//go:embed files/eng_ca.crt
152211
var defaultEngCA []byte
153212

@@ -161,39 +220,40 @@ type TLS struct {
161220
ServerKey string `json:"server_key"`
162221
}
163222

164-
// AirbrakeTLSConfig return the TLS config needed for connecting with airbrake server
165-
func (c *Config) AirbrakeTLSConfig() (*tls.Config, error) {
166-
if c.Airbrake.TLS == nil {
223+
// ClientTLSConfig builds a client-side *tls.Config from an optional cert pair
224+
// (mutual TLS) and CA file (server verification). A nil receiver yields a nil
225+
// config so callers can treat "no TLS" uniformly.
226+
func (t *TLS) ClientTLSConfig() (*tls.Config, error) {
227+
if t == nil {
167228
return nil, nil
168229
}
169-
caPath := c.Airbrake.TLS.CAFile
170-
certPath := c.Airbrake.TLS.ServerCert
171-
keyPath := c.Airbrake.TLS.ServerKey
172230
tlsConfig := &tls.Config{}
173-
if certPath != "" && keyPath != "" {
174-
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
231+
if t.ServerCert != "" && t.ServerKey != "" {
232+
cert, err := tls.LoadX509KeyPair(t.ServerCert, t.ServerKey)
175233
if err != nil {
176-
return nil, fmt.Errorf("can't properly load cert pair (%s, %s): %s", certPath, keyPath, err.Error())
234+
return nil, fmt.Errorf("can't properly load cert pair (%s, %s): %s", t.ServerCert, t.ServerKey, err.Error())
177235
}
178236
tlsConfig.Certificates = []tls.Certificate{cert}
179-
// TODO remove the lint bypass
180-
// nolint:staticcheck
181-
tlsConfig.BuildNameToCertificate()
182237
}
183238

184-
if caPath != "" {
185-
clientCACert, err := os.ReadFile(caPath)
239+
if t.CAFile != "" {
240+
caCert, err := os.ReadFile(t.CAFile)
186241
if err != nil {
187-
return nil, fmt.Errorf("can't properly load ca cert (%s): %s", caPath, err.Error())
242+
return nil, fmt.Errorf("can't properly load ca cert (%s): %s", t.CAFile, err.Error())
188243
}
189-
clientCertPool := x509.NewCertPool()
190-
clientCertPool.AppendCertsFromPEM(clientCACert)
191-
tlsConfig.RootCAs = clientCertPool
244+
caCertPool := x509.NewCertPool()
245+
caCertPool.AppendCertsFromPEM(caCert)
246+
tlsConfig.RootCAs = caCertPool
192247
}
193248

194249
return tlsConfig, nil
195250
}
196251

252+
// AirbrakeTLSConfig return the TLS config needed for connecting with airbrake server
253+
func (c *Config) AirbrakeTLSConfig() (*tls.Config, error) {
254+
return c.Airbrake.TLS.ClientTLSConfig()
255+
}
256+
197257
// VinsToTrack to track incoming signals in promemetheus
198258
func (c *Config) VinsToTrack() map[string]struct{} {
199259
output := make(map[string]struct{}, 0)
@@ -346,6 +406,21 @@ func (c *Config) ConfigureProducers(airbrakeHandler *airbrake.Handler, logger *l
346406
producers[telemetry.MQTT] = mqttProducer
347407
}
348408

409+
if _, ok := requiredDispatchers[telemetry.Redis]; ok {
410+
if c.Redis == nil {
411+
return nil, nil, errors.New("expected Redis to be configured")
412+
}
413+
options, err := c.Redis.options()
414+
if err != nil {
415+
return nil, nil, err
416+
}
417+
redisProducer, err := redisdatastore.NewProducer(options, c.Redis.PublishVINTopics, c.Redis.SubscriberSetPrefix, c.Namespace, c.prometheusEnabled(), c.MetricCollector, airbrakeHandler, c.AckChan, reliableAckSources[telemetry.Redis], logger)
418+
if err != nil {
419+
return nil, nil, err
420+
}
421+
producers[telemetry.Redis] = redisProducer
422+
}
423+
349424
dispatchProducerRules := make(map[string][]telemetry.Producer)
350425
for recordName, dispatchRules := range c.Records {
351426
var dispatchFuncs []telemetry.Producer

0 commit comments

Comments
 (0)