Skip to content

Commit 30e48cd

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 30e48cd

12 files changed

Lines changed: 660 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: 97 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,64 @@ 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, always publishes on the VIN set-key
166+
// channel.
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 channel if set.
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+
ReadTimeout time.Duration `json:"read_timeout,omitempty"`
182+
WriteTimeout time.Duration `json:"write_timeout,omitempty"`
183+
PoolTimeout time.Duration `json:"pool_timeout,omitempty"`
184+
ConnMaxIdleTime time.Duration `json:"conn_max_idle_time,omitempty"`
185+
ConnMaxLifetime time.Duration `json:"conn_max_lifetime,omitempty"`
186+
}
187+
188+
func (r *Redis) options() (*goredis.UniversalOptions, error) {
189+
tlsConfig, err := r.TLS.ClientTLSConfig()
190+
if err != nil {
191+
return nil, err
192+
}
193+
options := &goredis.UniversalOptions{
194+
Addrs: r.Addrs,
195+
Username: r.Username,
196+
Password: r.Password,
197+
DB: r.DB,
198+
TLSConfig: tlsConfig,
199+
}
200+
if r.Pool != nil {
201+
options.PoolSize = r.Pool.PoolSize
202+
options.MinIdleConns = r.Pool.MinIdleConns
203+
options.MaxIdleConns = r.Pool.MaxIdleConns
204+
options.MaxActiveConns = r.Pool.MaxActiveConns
205+
options.PoolTimeout = r.Pool.PoolTimeout
206+
options.ReadTimeout = r.Pool.ReadTimeout
207+
options.WriteTimeout = r.Pool.WriteTimeout
208+
options.ConnMaxIdleTime = r.Pool.ConnMaxIdleTime
209+
options.ConnMaxLifetime = r.Pool.ConnMaxLifetime
210+
}
211+
return options, nil
212+
}
213+
151214
//go:embed files/eng_ca.crt
152215
var defaultEngCA []byte
153216

@@ -161,39 +224,40 @@ type TLS struct {
161224
ServerKey string `json:"server_key"`
162225
}
163226

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

184-
if caPath != "" {
185-
clientCACert, err := os.ReadFile(caPath)
243+
if t.CAFile != "" {
244+
caCert, err := os.ReadFile(t.CAFile)
186245
if err != nil {
187-
return nil, fmt.Errorf("can't properly load ca cert (%s): %s", caPath, err.Error())
246+
return nil, fmt.Errorf("can't properly load ca cert (%s): %s", t.CAFile, err.Error())
188247
}
189-
clientCertPool := x509.NewCertPool()
190-
clientCertPool.AppendCertsFromPEM(clientCACert)
191-
tlsConfig.RootCAs = clientCertPool
248+
caCertPool := x509.NewCertPool()
249+
caCertPool.AppendCertsFromPEM(caCert)
250+
tlsConfig.RootCAs = caCertPool
192251
}
193252

194253
return tlsConfig, nil
195254
}
196255

256+
// AirbrakeTLSConfig return the TLS config needed for connecting with airbrake server
257+
func (c *Config) AirbrakeTLSConfig() (*tls.Config, error) {
258+
return c.Airbrake.TLS.ClientTLSConfig()
259+
}
260+
197261
// VinsToTrack to track incoming signals in promemetheus
198262
func (c *Config) VinsToTrack() map[string]struct{} {
199263
output := make(map[string]struct{}, 0)
@@ -346,6 +410,21 @@ func (c *Config) ConfigureProducers(airbrakeHandler *airbrake.Handler, logger *l
346410
producers[telemetry.MQTT] = mqttProducer
347411
}
348412

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

0 commit comments

Comments
 (0)