-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
151 lines (125 loc) · 4.18 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package main
import (
"bufio"
"context"
"flag"
"net/http"
"os"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/subchen/go-log"
"github.com/tarm/serial"
)
var (
VERSION = "0.0.1"
serialPort = flag.String("port", "/dev/ttyUSB0", "The serial device rflink is connected to.")
serialBaud = flag.Int("baud", 57600, "The baud rate of the serial connection.")
promAddr = flag.String("listen", ":8080", "The address to listen on for the Prometheus HTTP endpoint.")
mPathFlag = flag.String("namemap", "", "Mapping file to match sensors id with a name.")
verbose = flag.Bool("v", false, "Increase verbosity")
timeout = flag.Int("timeout", 180, "Number of seconds to wait before considering a sensor has disappeared")
// Nested map containing all the gauges. First dimension is the vendor name
// and the id of the sensor, joined by ' '. Second dimension is the type of
// data extracted from this sensor. Sensors can send multiple types of data
// at once.
// vendor+id type
sensors = map[string]map[string]*Metric{}
)
var mapping *Mapping
func startPromHttpServer(wg *sync.WaitGroup) *http.Server {
srv := &http.Server{Addr: *promAddr}
http.Handle("/metrics", promhttp.Handler())
go func() {
defer wg.Done() // let main know we are done cleaning up
// always returns error. ErrServerClosed on graceful close
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
// unexpected error. port in use?
log.Fatalf("Error while starting Prometheus HTTTP server: %s", err)
}
}()
// returning reference so caller can call Shutdown()
return srv
}
func main() {
flag.Parse()
log.Default.Level = log.INFO
if *verbose {
log.Default.Level = log.DEBUG
}
log.Infof("rflink-prom v%s -- Prometheus exporter for rflink", VERSION)
// Read mapping file if any
mappingPath := *mPathFlag
if mappingPath == "" {
mappingPath = "mapping.yaml"
}
mapping, err := readMapping(mappingPath)
if err != nil {
log.Warnf("Cannot parse mapping file %s: %v. Skipping", mappingPath, err)
}
if mapping != nil {
log.Infof("Using id to name mapping: %+v", mapping.IdNames)
}
// Setup prom HTTP server
httpServerExitDone := &sync.WaitGroup{}
httpServerExitDone.Add(1)
promSrv := startPromHttpServer(httpServerExitDone)
log.Infof("Serving prometheus metrics on %s", *promAddr)
// Setup serial port
port, err := serial.OpenPort(&serial.Config{
Name: *serialPort,
Baud: *serialBaud,
})
if err != nil {
log.Fatalf("Failed to open device %s: %v", *serialPort, err)
}
defer port.Close()
log.Infof("rflink connection established on %s", *serialPort)
reader := bufio.NewReader(port)
// Goroutine that reads rflink and creates/updates the prometheus metrics
// accordingly
go func() {
for {
line, _, err := reader.ReadLine()
if err != nil {
log.Errorf("Cannot read from serial: %v", err)
// TODO: properly shutdown promhttp via WaitGroup
// cf. https://stackoverflow.com/questions/39320025/how-to-stop-http-listenandserve
os.Exit(1)
}
log.Debugf("Received from rflink: %s", line)
err = updateMetrics(string(line), mapping)
if err != nil {
log.Errorf("Cannot update metrics from message: %v, skipping", err)
continue
}
}
}()
// Goroutine that expires prometheus metrics according to the timeout
// The loop is run every 1/4th of the timeout
// TODO: this is a naive approach that goes through all metrics in a loop
// It should be replaced by a more event-like system
go func() {
for {
log.Debugf("Checking all expired metrics after %d seconds of absence", *timeout)
for _, v := range sensors {
for _, m := range v {
m.EnforceExpiration()
}
}
time.Sleep(time.Second * time.Duration(*timeout) / 4)
}
}()
// wait for goroutine started in startHttpServer() to stop
httpServerExitDone.Wait()
log.Infof("Stopping prometheus exporter")
// 10 seconds timeout before forcing shutdown
d := time.Now().Add(5)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
// now close the server gracefully ("shutdown")
if err := promSrv.Shutdown(ctx); err != nil {
panic(err) // failure/timeout shutting down the server gracefully
}
log.Infof("Bye bye")
}