Skip to content

Commit 2fb9bc5

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 2fb9bc5

12 files changed

Lines changed: 656 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: 98 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,65 @@ 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+
PublishTimeout time.Duration `json:"publish_timeout,omitempty"`
165+
166+
// PublishVINTopics, when true, always publishes on the VIN set-key
167+
// channel.
168+
PublishVINTopics bool `json:"publish_vin_topics,omitempty"`
169+
170+
// SubscriberSetPrefix names the per-VIN sorted set of subscriber channels
171+
// (<prefix>_<namespace>_<txtype>_{<vin>}). When empty, the sorted set is not
172+
// consulted and records route only via the VIN channel if set.
173+
SubscriberSetPrefix string `json:"subscriber_set_prefix,omitempty"`
174+
}
175+
176+
// RedisPool configures the Redis connection pool. Zero values use go-redis defaults.
177+
type RedisPool struct {
178+
PoolSize int `json:"pool_size,omitempty"`
179+
MinIdleConns int `json:"min_idle_conns,omitempty"`
180+
MaxIdleConns int `json:"max_idle_conns,omitempty"`
181+
MaxActiveConns int `json:"max_active_conns,omitempty"`
182+
ReadTimeout time.Duration `json:"read_timeout,omitempty"`
183+
WriteTimeout time.Duration `json:"write_timeout,omitempty"`
184+
PoolTimeout time.Duration `json:"pool_timeout,omitempty"`
185+
ConnMaxIdleTime time.Duration `json:"conn_max_idle_time,omitempty"`
186+
ConnMaxLifetime time.Duration `json:"conn_max_lifetime,omitempty"`
187+
}
188+
189+
func (r *Redis) options() (*goredis.UniversalOptions, error) {
190+
tlsConfig, err := r.TLS.ClientTLSConfig()
191+
if err != nil {
192+
return nil, err
193+
}
194+
options := &goredis.UniversalOptions{
195+
Addrs: r.Addrs,
196+
Username: r.Username,
197+
Password: r.Password,
198+
DB: r.DB,
199+
TLSConfig: tlsConfig,
200+
}
201+
if r.Pool != nil {
202+
options.PoolSize = r.Pool.PoolSize
203+
options.MinIdleConns = r.Pool.MinIdleConns
204+
options.MaxIdleConns = r.Pool.MaxIdleConns
205+
options.MaxActiveConns = r.Pool.MaxActiveConns
206+
options.PoolTimeout = r.Pool.PoolTimeout
207+
options.ReadTimeout = r.Pool.ReadTimeout
208+
options.WriteTimeout = r.Pool.WriteTimeout
209+
options.ConnMaxIdleTime = r.Pool.ConnMaxIdleTime
210+
options.ConnMaxLifetime = r.Pool.ConnMaxLifetime
211+
}
212+
return options, nil
213+
}
214+
151215
//go:embed files/eng_ca.crt
152216
var defaultEngCA []byte
153217

@@ -161,39 +225,40 @@ type TLS struct {
161225
ServerKey string `json:"server_key"`
162226
}
163227

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 {
228+
// ClientTLSConfig builds a client-side *tls.Config from an optional cert pair
229+
// (mutual TLS) and CA file (server verification). A nil receiver yields a nil
230+
// config so callers can treat "no TLS" uniformly.
231+
func (t *TLS) ClientTLSConfig() (*tls.Config, error) {
232+
if t == nil {
167233
return nil, nil
168234
}
169-
caPath := c.Airbrake.TLS.CAFile
170-
certPath := c.Airbrake.TLS.ServerCert
171-
keyPath := c.Airbrake.TLS.ServerKey
172235
tlsConfig := &tls.Config{}
173-
if certPath != "" && keyPath != "" {
174-
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
236+
if t.ServerCert != "" && t.ServerKey != "" {
237+
cert, err := tls.LoadX509KeyPair(t.ServerCert, t.ServerKey)
175238
if err != nil {
176-
return nil, fmt.Errorf("can't properly load cert pair (%s, %s): %s", certPath, keyPath, err.Error())
239+
return nil, fmt.Errorf("can't properly load cert pair (%s, %s): %s", t.ServerCert, t.ServerKey, err.Error())
177240
}
178241
tlsConfig.Certificates = []tls.Certificate{cert}
179-
// TODO remove the lint bypass
180-
// nolint:staticcheck
181-
tlsConfig.BuildNameToCertificate()
182242
}
183243

184-
if caPath != "" {
185-
clientCACert, err := os.ReadFile(caPath)
244+
if t.CAFile != "" {
245+
caCert, err := os.ReadFile(t.CAFile)
186246
if err != nil {
187-
return nil, fmt.Errorf("can't properly load ca cert (%s): %s", caPath, err.Error())
247+
return nil, fmt.Errorf("can't properly load ca cert (%s): %s", t.CAFile, err.Error())
188248
}
189-
clientCertPool := x509.NewCertPool()
190-
clientCertPool.AppendCertsFromPEM(clientCACert)
191-
tlsConfig.RootCAs = clientCertPool
249+
caCertPool := x509.NewCertPool()
250+
caCertPool.AppendCertsFromPEM(caCert)
251+
tlsConfig.RootCAs = caCertPool
192252
}
193253

194254
return tlsConfig, nil
195255
}
196256

257+
// AirbrakeTLSConfig return the TLS config needed for connecting with airbrake server
258+
func (c *Config) AirbrakeTLSConfig() (*tls.Config, error) {
259+
return c.Airbrake.TLS.ClientTLSConfig()
260+
}
261+
197262
// VinsToTrack to track incoming signals in promemetheus
198263
func (c *Config) VinsToTrack() map[string]struct{} {
199264
output := make(map[string]struct{}, 0)
@@ -346,6 +411,21 @@ func (c *Config) ConfigureProducers(airbrakeHandler *airbrake.Handler, logger *l
346411
producers[telemetry.MQTT] = mqttProducer
347412
}
348413

414+
if _, ok := requiredDispatchers[telemetry.Redis]; ok {
415+
if c.Redis == nil {
416+
return nil, nil, errors.New("expected Redis to be configured")
417+
}
418+
options, err := c.Redis.options()
419+
if err != nil {
420+
return nil, nil, err
421+
}
422+
redisProducer, err := redisdatastore.NewProducer(options, c.Redis.PublishTimeout, c.Redis.PublishVINTopics, c.Redis.SubscriberSetPrefix, c.Namespace, c.prometheusEnabled(), c.MetricCollector, airbrakeHandler, c.AckChan, reliableAckSources[telemetry.Redis], logger)
423+
if err != nil {
424+
return nil, nil, err
425+
}
426+
producers[telemetry.Redis] = redisProducer
427+
}
428+
349429
dispatchProducerRules := make(map[string][]telemetry.Producer)
350430
for recordName, dispatchRules := range c.Records {
351431
var dispatchFuncs []telemetry.Producer

0 commit comments

Comments
 (0)