Add InfluxDB exporter

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2022-11-20 14:44:12 +01:00
parent 75ba452149
commit fcc21cfb75
Signed by: luzifer
GPG key ID: D91C3E91E4CAD6F5
12 changed files with 292 additions and 9 deletions

View file

@ -17,6 +17,7 @@ Usage of mercedes-byocar-exporter:
--client-secret string Client-Secret of Mercedes Developers Console App
--credential-file string Where to store tokens when using client-id from CLI parameters (default "credentials.json")
--fetch-interval duration How often to ask the Mercedes API for updates (default 15m0s)
--influx-export string Set to url (http[s]://user:pass@host[:port]/database) to enable Influx exporter
--listen string Port/IP to listen on (default ":3000")
--log-level string Log level (debug, info, warn, error, fatal) (default "info")
--redirect-url string Redirect URL registered in Mercedes Developers Console (default "http://127.0.0.1:3000/store-token")

View file

@ -11,6 +11,7 @@ type (
ClientSecret string `flag:"client-secret" default:"" description:"Client-Secret of Mercedes Developers Console App"`
CredentialFile string `flag:"credential-file" default:"credentials.json" description:"Where to store tokens when using client-id from CLI parameters"`
FetchInterval time.Duration `flag:"fetch-interval" default:"15m" description:"How often to ask the Mercedes API for updates"`
InfluxExport string `flag:"influx-export" default:"" description:"Set to url (http[s]://user:pass@host[:port]/database) to enable Influx exporter"`
Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"`
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
RedirectURL string `flag:"redirect-url" default:"http://127.0.0.1:3000/store-token" description:"Redirect URL registered in Mercedes Developers Console"`

View file

@ -4,7 +4,6 @@ import (
"github.com/sirupsen/logrus"
"github.com/Luzifer/mercedes-byocar-exporter/internal/mercedes"
"github.com/Luzifer/mercedes-byocar-exporter/internal/prometheus"
)
func getCronFunc(mc mercedes.Client) func() {
@ -24,28 +23,28 @@ func runFetcher(mc mercedes.Client, vehicleID string) {
logger.WithError(err).Error("fetching pay-as-you-go data")
return
}
prometheus.SetPayAsYouGo(vehicleID, s1)
enabledExporters.SetPayAsYouGo(vehicleID, s1)
s2, err := mc.GetFuelStatus(cfg.VehicleID[0])
if err != nil {
logger.WithError(err).Error("fetching fuel-status data")
return
}
prometheus.SetFuelStatus(vehicleID, s2)
enabledExporters.SetFuelStatus(vehicleID, s2)
s3, err := mc.GetVehicleStatus(cfg.VehicleID[0])
if err != nil {
logger.WithError(err).Error("fetching vehicle-status data")
return
}
prometheus.SetVehicleStatus(vehicleID, s3)
enabledExporters.SetVehicleStatus(vehicleID, s3)
s4, err := mc.GetLockStatus(cfg.VehicleID[0])
if err != nil {
logger.WithError(err).Error("fetching lock-status data")
return
}
prometheus.SetLockStatus(vehicleID, s4)
enabledExporters.SetLockStatus(vehicleID, s4)
logger.Info("data updated successfully")
}

1
go.mod
View file

@ -6,6 +6,7 @@ require (
github.com/Luzifer/rconfig/v2 v2.4.0
github.com/gofrs/uuid v4.3.1+incompatible
github.com/hashicorp/vault/api v1.8.2
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.14.0

2
go.sum
View file

@ -204,6 +204,8 @@ github.com/hashicorp/vault/sdk v0.6.0/go.mod h1:+DRpzoXIdMvKc88R4qxr+edwy/RvH5QK
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs=
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=

View file

@ -0,0 +1,112 @@
package influxdb
import (
"net/url"
"strings"
"sync"
"time"
influx "github.com/influxdata/influxdb1-client/v2"
"github.com/pkg/errors"
"github.com/Luzifer/mercedes-byocar-exporter/internal/exporters"
)
const (
influxTimeout = 2 * time.Second
influxWriteInterval = 10 * time.Second
)
type (
Exporter struct {
batch influx.BatchPoints
batchLock sync.Mutex
client influx.Client
database string
errs chan error
}
)
var _ exporters.Exporter = (*Exporter)(nil)
func New(connURL string) (*Exporter, error) {
out := &Exporter{
errs: make(chan error, 10), //nolint: gomnd // Is a constant but makes no sense to name
}
return out, out.initialize(connURL)
}
func (e *Exporter) Errors() <-chan error {
return e.errs
}
func (e *Exporter) RecordPoint(name string, tags map[string]string, fields map[string]interface{}, updatedAt time.Time) error {
pt, err := influx.NewPoint(name, tags, fields, updatedAt)
if err != nil {
return err
}
e.batchLock.Lock()
defer e.batchLock.Unlock()
e.batch.AddPoint(pt)
return nil
}
func (e *Exporter) resetBatch() error {
b, err := influx.NewBatchPoints(influx.BatchPointsConfig{
Database: e.database,
})
if err != nil {
return err
}
e.batch = b
return nil
}
func (e *Exporter) sendLoop() {
for range time.Tick(influxWriteInterval) {
e.batchLock.Lock()
if err := e.client.Write(e.batch); err != nil {
e.errs <- err
e.batchLock.Unlock()
continue
}
e.resetBatch()
e.batchLock.Unlock()
}
}
func (e *Exporter) initialize(connURL string) error {
connInfo, err := url.Parse(connURL)
if err != nil {
return errors.Wrap(err, "parsing connection URL")
}
e.database = strings.TrimLeft(connInfo.Path, "/")
cfg := influx.HTTPConfig{
Addr: (&url.URL{Scheme: connInfo.Scheme, Host: connInfo.Host}).String(),
Timeout: influxTimeout,
}
if connInfo.User != nil {
cfg.Username = connInfo.User.Username()
cfg.Password = func(pass string, _ bool) string { return pass }(connInfo.User.Password())
}
influxClient, err := influx.NewHTTPClient(cfg)
if err != nil {
return err
}
e.client = influxClient
if err := e.resetBatch(); err != nil {
return err
}
go e.sendLoop()
return nil
}

View file

@ -0,0 +1,87 @@
package influxdb
import (
"strings"
"github.com/Luzifer/mercedes-byocar-exporter/internal/mercedes"
)
const (
labelVehicleID = "vehicle_id"
labelDoor = "door"
labelLight = "light"
labelWindow = "window"
subsystemFuelStatus = "fuel_status"
subsystemLockStatus = "lock_status"
subsystemPayAsYouDrive = "pay_as_you_drive"
subsystemVehicleStatus = "vehicle_status"
)
func (e *Exporter) SetFuelStatus(vehicleID string, fs mercedes.FuelStatus) {
e.submitValue(fs.RangeLiquid, mn(subsystemFuelStatus, "range_liquid"), labelVehicleID, vehicleID)
e.submitValue(fs.TanklevelPercent, mn(subsystemFuelStatus, "tanklevel_percent"), labelVehicleID, vehicleID)
}
func (e *Exporter) SetLockStatus(vehicleID string, ls mercedes.LockStatus) {
e.submitValue(ls.DeckLidUnlocked, mn(subsystemLockStatus, "deck_lid_unlocked"), labelVehicleID, vehicleID)
e.submitValue(ls.VehicleStatus, mn(subsystemLockStatus, "vehicle_status"), labelVehicleID, vehicleID)
e.submitValue(ls.GasLidUnlocked, mn(subsystemLockStatus, "gas_lid_unlocked"), labelVehicleID, vehicleID)
e.submitValue(ls.Heading, mn(subsystemLockStatus, "heading"), labelVehicleID, vehicleID)
}
func (e *Exporter) SetPayAsYouGo(vehicleID string, p mercedes.PayAsYouDriveInsurance) {
e.submitValue(p.Odometer, mn(subsystemPayAsYouDrive, "odometer"), labelVehicleID, vehicleID)
}
func (e *Exporter) SetVehicleStatus(vehicleID string, vs mercedes.VehicleStatus) {
e.submitValue(vs.DeckLidOpen, mn(subsystemVehicleStatus, "deck_lid_open"), labelVehicleID, vehicleID)
e.submitValue(vs.DoorFrontLeftOpen, mn(subsystemVehicleStatus, "door_open"), labelVehicleID, vehicleID, labelDoor, "front_left")
e.submitValue(vs.DoorFrontRightOpen, mn(subsystemVehicleStatus, "door_open"), labelVehicleID, vehicleID, labelDoor, "front_right")
e.submitValue(vs.DoorRearLeftOpen, mn(subsystemVehicleStatus, "door_open"), labelVehicleID, vehicleID, labelDoor, "rear_left")
e.submitValue(vs.DoorRearRightOpen, mn(subsystemVehicleStatus, "door_open"), labelVehicleID, vehicleID, labelDoor, "rear_right")
e.submitValue(vs.InteriorLightsFrontOn, mn(subsystemVehicleStatus, "interior_light_on"), labelVehicleID, vehicleID, labelLight, "front")
e.submitValue(vs.InteriorLightsRearOn, mn(subsystemVehicleStatus, "interior_light_on"), labelVehicleID, vehicleID, labelLight, "rear")
e.submitValue(vs.LightSwitchPosition, mn(subsystemVehicleStatus, "light_switch_position"), labelVehicleID, vehicleID)
e.submitValue(vs.ReadingLampFrontLeftOn, mn(subsystemVehicleStatus, "reading_lamp_on"), labelVehicleID, vehicleID, labelLight, "front_left")
e.submitValue(vs.ReadingLampFrontRightOn, mn(subsystemVehicleStatus, "reading_lamp_on"), labelVehicleID, vehicleID, labelLight, "front_right")
e.submitValue(vs.RoofTopStatus, mn(subsystemVehicleStatus, "roof_top_status"), labelVehicleID, vehicleID)
e.submitValue(vs.SunRoofStatus, mn(subsystemVehicleStatus, "sun_roof_status"), labelVehicleID, vehicleID)
e.submitValue(vs.WindowStatusFrontLeft, mn(subsystemVehicleStatus, "window_status"), labelVehicleID, vehicleID, labelWindow, "front_left")
e.submitValue(vs.WindowStatusFrontRight, mn(subsystemVehicleStatus, "window_status"), labelVehicleID, vehicleID, labelWindow, "front_right")
e.submitValue(vs.WindowStatusRearLeft, mn(subsystemVehicleStatus, "window_status"), labelVehicleID, vehicleID, labelWindow, "rear_left")
e.submitValue(vs.WindowStatusRearRight, mn(subsystemVehicleStatus, "window_status"), labelVehicleID, vehicleID, labelWindow, "rear_right")
}
func (e *Exporter) submitValue(value mercedes.MetricValue, metric_name string, tvs ...string) {
if !value.IsValid() {
return
}
v := map[string]any{"value": value.ToFloat()}
e.RecordPoint(metric_name, tags(tvs...), v, value.Time())
}
func mn(parts ...string) string {
return strings.Join(parts, "_")
}
func tags(kvs ...string) map[string]string {
out := make(map[string]string)
if len(kvs)%2 != 0 {
panic("invalid tags given")
}
for i := 0; i < len(kvs); i += 2 {
out[kvs[i]] = kvs[i+1]
}
return out
}

View file

@ -0,0 +1,40 @@
package exporters
import "github.com/Luzifer/mercedes-byocar-exporter/internal/mercedes"
type (
Exporter interface {
SetFuelStatus(vehicleID string, fs mercedes.FuelStatus)
SetLockStatus(vehicleID string, ls mercedes.LockStatus)
SetPayAsYouGo(vehicleID string, p mercedes.PayAsYouDriveInsurance)
SetVehicleStatus(vehicleID string, vs mercedes.VehicleStatus)
}
Set []Exporter
)
var _ Exporter = Set{}
func (s Set) SetFuelStatus(vehicleID string, fs mercedes.FuelStatus) {
for _, e := range s {
e.SetFuelStatus(vehicleID, fs)
}
}
func (s Set) SetLockStatus(vehicleID string, ls mercedes.LockStatus) {
for _, e := range s {
e.SetLockStatus(vehicleID, ls)
}
}
func (s Set) SetPayAsYouGo(vehicleID string, p mercedes.PayAsYouDriveInsurance) {
for _, e := range s {
e.SetPayAsYouGo(vehicleID, p)
}
}
func (s Set) SetVehicleStatus(vehicleID string, vs mercedes.VehicleStatus) {
for _, e := range s {
e.SetVehicleStatus(vehicleID, vs)
}
}

View file

@ -3,26 +3,36 @@ package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/Luzifer/mercedes-byocar-exporter/internal/exporters"
"github.com/Luzifer/mercedes-byocar-exporter/internal/mercedes"
)
func SetFuelStatus(vehicleID string, fs mercedes.FuelStatus) {
type (
exporter struct{}
)
var (
Exporter exporter
_ exporters.Exporter = exporter{}
)
func (exporter) SetFuelStatus(vehicleID string, fs mercedes.FuelStatus) {
setGaugeVecValue(fs.RangeLiquid, fuelRangeLiquidVec, labelVehicleID, vehicleID)
setGaugeVecValue(fs.TanklevelPercent, fuelTanklevelPercent, labelVehicleID, vehicleID)
}
func SetLockStatus(vehicleID string, ls mercedes.LockStatus) {
func (exporter) SetLockStatus(vehicleID string, ls mercedes.LockStatus) {
setGaugeVecValue(ls.DeckLidUnlocked, lockDeckLidUnlocked, labelVehicleID, vehicleID)
setGaugeVecValue(ls.VehicleStatus, lockVehicleStatus, labelVehicleID, vehicleID)
setGaugeVecValue(ls.GasLidUnlocked, lockGasLidUnlocked, labelVehicleID, vehicleID)
setGaugeVecValue(ls.Heading, lockHeading, labelVehicleID, vehicleID)
}
func SetPayAsYouGo(vehicleID string, p mercedes.PayAsYouDriveInsurance) {
func (exporter) SetPayAsYouGo(vehicleID string, p mercedes.PayAsYouDriveInsurance) {
setGaugeVecValue(p.Odometer, paydOdometer, labelVehicleID, vehicleID)
}
func SetVehicleStatus(vehicleID string, vs mercedes.VehicleStatus) {
func (exporter) SetVehicleStatus(vehicleID string, vs mercedes.VehicleStatus) {
setGaugeVecValue(vs.DeckLidOpen, vehicleDeckLidOpen, labelVehicleID, vehicleID)
setGaugeVecValue(vs.DoorFrontLeftOpen, vehicleDoorOpen, labelVehicleID, vehicleID, labelDoor, "front_left")

View file

@ -18,6 +18,7 @@ type (
MetricValue interface {
IsValid() bool
Time() time.Time
ToFloat() float64
}
@ -89,6 +90,8 @@ func (t TimedBool) IsValid() bool { return !t.t.IsZero() }
func (t TimedBool) String() string { return fmt.Sprintf("%v (%s)", t.v, t.t.Format(time.RFC3339)) }
func (t TimedBool) Time() time.Time { return t.t }
func (t TimedBool) ToFloat() float64 {
if t.v {
return 1
@ -110,6 +113,8 @@ func (t TimedEnum) String() string {
return fmt.Sprintf("%s (%s)", s, t.t.Format(time.RFC3339))
}
func (t TimedEnum) Time() time.Time { return t.t }
func (t TimedEnum) ToFloat() float64 { return float64(t.v) }
func (t TimedEnum) Value() string {
@ -128,6 +133,8 @@ func (t TimedFloat) IsValid() bool { return !t.t.IsZero() }
func (t TimedFloat) String() string { return fmt.Sprintf("%v (%s)", t.v, t.t.Format(time.RFC3339)) }
func (t TimedFloat) Time() time.Time { return t.t }
func (t TimedFloat) ToFloat() float64 { return t.v }
// Int
@ -138,4 +145,6 @@ func (t TimedInt) IsValid() bool { return !t.t.IsZero() }
func (t TimedInt) String() string { return fmt.Sprintf("%v (%s)", t.v, t.t.Format(time.RFC3339)) }
func (t TimedInt) Time() time.Time { return t.t }
func (t TimedInt) ToFloat() float64 { return float64(t.v) }

21
main.go
View file

@ -12,6 +12,9 @@ import (
"github.com/sirupsen/logrus"
"github.com/Luzifer/mercedes-byocar-exporter/internal/credential"
"github.com/Luzifer/mercedes-byocar-exporter/internal/exporters"
"github.com/Luzifer/mercedes-byocar-exporter/internal/exporters/influxdb"
"github.com/Luzifer/mercedes-byocar-exporter/internal/exporters/prometheus"
"github.com/Luzifer/mercedes-byocar-exporter/internal/mercedes"
"github.com/Luzifer/rconfig/v2"
)
@ -19,6 +22,8 @@ import (
var (
cfg cliConfig
version = "dev"
enabledExporters exporters.Set
)
func initApp() error {
@ -73,6 +78,22 @@ func main() {
}
mClient := mercedes.New(clientID, clientSecret, creds)
// Register Exporters
enabledExporters = append(enabledExporters, prometheus.Exporter)
if cfg.InfluxExport != "" {
logrus.Info("creating influxdb exporter")
influxExporter, err := influxdb.New(cfg.InfluxExport)
if err != nil {
logrus.WithError(err).Fatal("creating influxdb exporter")
}
go func() {
for err := range influxExporter.Errors() {
logrus.WithError(err).Error("processing influx metrics")
}
}()
enabledExporters = append(enabledExporters, influxExporter)
}
// Register HTTP handlers
http.DefaultServeMux.HandleFunc("/auth", getAuthRedirectHandler(mClient))
http.DefaultServeMux.HandleFunc("/store-token", getAuthStoreTokenHandler(mClient, creds))