-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
198 lines (163 loc) · 4.8 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package main
// I'm a long standing C developer learning Go
// Any comments on more idiomatic Go are welcome
// Prometheus Exporter for Elgato (Keylight at first, more to come)
// Example output
// {"numberOfLights":1,"lights":[{"on":1,"brightness":55,"temperature":198}]}
// TODO - Need the ability to set labels for a particular light (IP/port) to give it a location
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// JSON structures:
//
// {"numberOfLights":1,"lights":[{"on":1,"brightness":55,"temperature":198}]}
type elgatoLight struct {
On int
Brightness int
Temperature int
}
type elgatoResponse struct {
NumberOfLights int
Lights []elgatoLight
}
var (
hc *http.Client
cfg Config
recorder Recorder
)
type Config struct {
timeout time.Duration // timeout in msec
ipaddress string // IP address
port int // port
metricport int // metricport
pollinterval time.Duration // polling interval
pollurl string // url for polling
metricurl string // url for metrics
datastore string // datastore ("" means no storage)
file string // filename to parse straight away
}
func RegisterFlags() {
flag.DurationVar(&cfg.timeout, "timeout", 1*time.Second, "Timeout for polling light")
flag.StringVar(&cfg.ipaddress, "ipaddress", "192.168.1.209", "IP Address of light")
flag.IntVar(&cfg.port, "port", 9123, "Port of light")
flag.IntVar(&cfg.metricport, "metricport", 9091, "port for serving metrics")
flag.DurationVar(&cfg.pollinterval, "interval", 10*time.Second, "Polling interval")
flag.StringVar(&cfg.pollurl, "pollurl", "elgato/lights", "URL to poll")
flag.StringVar(&cfg.metricurl, "metricurl", "/metrics", "URL to server metrics")
flag.StringVar(&cfg.datastore, "datastore", "", "Datastore directory, blank to disable")
flag.StringVar(&cfg.file, "file", "", "Parse file directly")
}
func parseJSON(body string) error {
var r elgatoResponse
parseTime := time.Now()
err := json.Unmarshal([]byte(body), &r)
durParse := time.Since(parseTime)
recorder.measureParseDur(durParse)
if err != nil {
// TODO - do something with err.Error() ?
recorder.measureLastError(parseTime)
log.Fatal("parsing:", err)
return err
}
// TODO - check values are within bounds?
// TODO - deal with numbers of lights?
recorder.measureOnOff(r.Lights[0].On)
recorder.measureBrightness(r.Lights[0].Brightness)
recorder.measureTemperature(r.Lights[0].Temperature)
return nil
}
func doPoll() {
url := fmt.Sprintf("http://%s:%d/%s", cfg.ipaddress, cfg.port, cfg.pollurl)
pollTime := time.Now()
req, err := http.NewRequest("GET", url, nil)
resp, err := hc.Do(req)
durPoll := time.Since(pollTime)
recorder.measureLastPoll(pollTime)
recorder.measurePollDur(durPoll)
// fmt.Printf("statusCode=%d\n", resp.StatusCode)
if err != nil {
fmt.Printf("Got error [%s]\n", err)
// TODO - do anything with err.Error() ?
recorder.measureLastError(pollTime)
recorder.measurePolls("error")
return
}
// Got a good response!
defer resp.Body.Close()
// Log the statusCode
recorder.measureStatusCode(resp.StatusCode)
body, err := io.ReadAll(resp.Body)
if err != nil {
// TODO
recorder.measurePolls("error-readall")
return
}
recorder.measureLastGoodPoll(pollTime)
// Stash the downloaded file if we have a datastore configured
if cfg.datastore != "" {
fname := cfg.datastore + "/" + pollTime.Format("20060102150405.000000")
os.WriteFile(fname, body, 0644)
}
// parse file and update values
parseTime := time.Now()
err = parseJSON(string(body))
durParse := time.Since(parseTime)
recorder.measureParseDur(durParse)
if err != nil {
// TODO Do something
recorder.measureLastError(pollTime)
recorder.measurePolls("parse-error")
return
}
recorder.measurePolls("ok")
}
func main() {
RegisterFlags()
flag.Parse()
recorder = NewRecorder(prometheus.DefaultRegisterer) // TODO - prefix
// check if we are parsing a single file
if cfg.file != "" {
// TODO read in file
body, err := os.ReadFile(cfg.file)
if err != nil {
log.Fatal("ReadFile:", err)
}
// parse it
err = parseJSON(string(body))
if err != nil {
log.Fatal("parseJSON:", err)
}
// output data
// output := metricText()
// fmt.Printf("%s", output)
return
}
// Serve Prom metrics on cfg.metricport
go func() {
listenaddr := ":" + strconv.Itoa(cfg.metricport)
http.Handle("/metrics", promhttp.Handler())
err := http.ListenAndServe(listenaddr, nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}()
// Setup timeout on http client
hc = &http.Client{
Timeout: cfg.timeout,
}
// Infinite loop of polling
for {
doPoll()
time.Sleep(cfg.pollinterval)
}
}