|
|
@ -25,6 +25,7 @@ import ( |
|
|
|
"io/ioutil" |
|
|
|
"sort" |
|
|
|
"bytes" |
|
|
|
"errors" |
|
|
|
|
|
|
|
"github.com/namsral/flag" |
|
|
|
"github.com/prometheus/client_golang/prometheus" |
|
|
@ -37,6 +38,7 @@ const serviceLoadRetryTime = 1 * time.Minute |
|
|
|
|
|
|
|
var ( |
|
|
|
flag_test = flag.Bool("test", false, "print all available metrics to stdout") |
|
|
|
flag_collect = flag.Bool("collect", false, "print configured metrics to stdout and exit") |
|
|
|
flag_jsonout = flag.String("json-out", "", "store metrics also to JSON file when running test") |
|
|
|
|
|
|
|
flag_addr = flag.String("listen-address", "127.0.0.1:9042", "The address to listen on for HTTP requests.") |
|
|
@ -61,10 +63,18 @@ type JSON_PromDesc struct { |
|
|
|
VarLabels []string `json:"varLabels"` |
|
|
|
} |
|
|
|
|
|
|
|
type ActionArg struct { |
|
|
|
Name string `json:"Name"` |
|
|
|
IsIndex bool `json:"IsIndex"` |
|
|
|
ProviderAction string `json:"ProviderAction"` |
|
|
|
Value string `json:"Value"` |
|
|
|
} |
|
|
|
|
|
|
|
type Metric struct { |
|
|
|
// initialized loading JSON
|
|
|
|
Service string `json:"service"` |
|
|
|
Action string `json:"action"` |
|
|
|
ActionArgument *ActionArg `json:"actionArgument"` |
|
|
|
Result string `json:"result"` |
|
|
|
OkValue string `json:"okValue"` |
|
|
|
PromDesc JSON_PromDesc `json:"promDesc"` |
|
|
@ -87,6 +97,29 @@ type FritzboxCollector struct { |
|
|
|
Root *upnp.Root |
|
|
|
} |
|
|
|
|
|
|
|
// simple ResponseWriter to collect output
|
|
|
|
type TestResponseWriter struct { |
|
|
|
header http.Header |
|
|
|
statusCode int |
|
|
|
body bytes.Buffer |
|
|
|
} |
|
|
|
|
|
|
|
func (w *TestResponseWriter) Header() http.Header { |
|
|
|
return w.header |
|
|
|
} |
|
|
|
|
|
|
|
func (w *TestResponseWriter) Write(b []byte) (int, error) { |
|
|
|
return w.body.Write(b) |
|
|
|
} |
|
|
|
|
|
|
|
func (w *TestResponseWriter) WriteHeader(statusCode int) { |
|
|
|
w.statusCode = statusCode |
|
|
|
} |
|
|
|
|
|
|
|
func (w *TestResponseWriter) String() string { |
|
|
|
return w.body.String() |
|
|
|
} |
|
|
|
|
|
|
|
// LoadServices tries to load the service information. Retries until success.
|
|
|
|
func (fc *FritzboxCollector) LoadServices() { |
|
|
|
for { |
|
|
@ -113,6 +146,72 @@ func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric, val interface{}) { |
|
|
|
var floatval float64 |
|
|
|
switch tval := val.(type) { |
|
|
|
case uint64: |
|
|
|
floatval = float64(tval) |
|
|
|
case bool: |
|
|
|
if tval { |
|
|
|
floatval = 1 |
|
|
|
} else { |
|
|
|
floatval = 0 |
|
|
|
} |
|
|
|
case string: |
|
|
|
if tval == m.OkValue { |
|
|
|
floatval = 1 |
|
|
|
} else { |
|
|
|
floatval = 0 |
|
|
|
} |
|
|
|
default: |
|
|
|
fmt.Println("unknown type", val) |
|
|
|
collect_errors.Inc() |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
ch <- prometheus.MustNewConstMetric( |
|
|
|
m.Desc, |
|
|
|
m.MetricType, |
|
|
|
floatval, |
|
|
|
fc.Gateway, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
func (fc *FritzboxCollector) GetActionResult(result_map map[string]upnp.Result, serviceType string, actionName string, actionArg *upnp.ActionArgument) (upnp.Result, error) { |
|
|
|
|
|
|
|
m_key := serviceType+"|"+actionName |
|
|
|
|
|
|
|
// for calls with argument also add arguement name and value to key
|
|
|
|
if actionArg != nil { |
|
|
|
|
|
|
|
m_key += "|"+actionArg.Name+"|"+fmt.Sprintf("%v", actionArg.Value) |
|
|
|
} |
|
|
|
|
|
|
|
last_result := result_map[m_key]; |
|
|
|
if last_result == nil { |
|
|
|
service, ok := fc.Root.Services[serviceType] |
|
|
|
if !ok { |
|
|
|
return nil, errors.New(fmt.Sprintf("service %s not found", serviceType)) |
|
|
|
} |
|
|
|
|
|
|
|
action, ok := service.Actions[actionName] |
|
|
|
if !ok { |
|
|
|
return nil, errors.New(fmt.Sprintf("action %s not found in service %s", actionName, serviceType)) |
|
|
|
} |
|
|
|
|
|
|
|
var err error |
|
|
|
last_result, err = action.Call(actionArg); |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
|
|
|
|
result_map[m_key]=last_result |
|
|
|
} |
|
|
|
|
|
|
|
return last_result, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { |
|
|
|
fc.Lock() |
|
|
|
root := fc.Root |
|
|
@ -123,73 +222,57 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
var err error |
|
|
|
// create a map for caching results
|
|
|
|
var result_map = make(map[string]upnp.Result) |
|
|
|
|
|
|
|
for _, m := range metrics { |
|
|
|
m_key := m.Service+"|"+m.Action |
|
|
|
last_result := result_map[m_key]; |
|
|
|
if last_result == nil { |
|
|
|
service, ok := root.Services[m.Service] |
|
|
|
if !ok { |
|
|
|
// TODO
|
|
|
|
fmt.Println("cannot find service", m.Service) |
|
|
|
fmt.Println(root.Services) |
|
|
|
continue |
|
|
|
} |
|
|
|
action, ok := service.Actions[m.Action] |
|
|
|
if !ok { |
|
|
|
// TODO
|
|
|
|
fmt.Println("cannot find action", m.Action) |
|
|
|
continue |
|
|
|
} |
|
|
|
var actArg *upnp.ActionArgument |
|
|
|
if m.ActionArgument != nil { |
|
|
|
aa := m.ActionArgument |
|
|
|
var value interface {} |
|
|
|
value = aa.Value |
|
|
|
|
|
|
|
if aa.ProviderAction != "" { |
|
|
|
provRes, err := fc.GetActionResult(result_map, m.Service, aa.ProviderAction, nil) |
|
|
|
|
|
|
|
last_result, err = action.Call() |
|
|
|
if err != nil { |
|
|
|
fmt.Println(err) |
|
|
|
fmt.Printf("Error getting provider action %s result for %s.%s: %s\n", aa.ProviderAction, m.Service, m.Action, err.Error()) |
|
|
|
collect_errors.Inc() |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
result_map[m_key]=last_result |
|
|
|
} |
|
|
|
|
|
|
|
val, ok := last_result[m.Result] |
|
|
|
var ok bool |
|
|
|
value, ok = provRes[aa.Value] // Value contains the result name for provider actions
|
|
|
|
if !ok { |
|
|
|
fmt.Println("result not found", m.Result) |
|
|
|
fmt.Printf("provider action %s for %s.%s has no result %s", m.Service, m.Action, aa.Value) |
|
|
|
collect_errors.Inc() |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var floatval float64 |
|
|
|
switch tval := val.(type) { |
|
|
|
case uint64: |
|
|
|
floatval = float64(tval) |
|
|
|
case bool: |
|
|
|
if tval { |
|
|
|
floatval = 1 |
|
|
|
if aa.IsIndex { |
|
|
|
// TODO handle index iterations
|
|
|
|
} else { |
|
|
|
floatval = 0 |
|
|
|
actArg = &upnp.ActionArgument{Name: aa.Name, Value: value } |
|
|
|
} |
|
|
|
case string: |
|
|
|
if tval == m.OkValue { |
|
|
|
floatval = 1 |
|
|
|
} else { |
|
|
|
floatval = 0 |
|
|
|
} |
|
|
|
default: |
|
|
|
fmt.Println("unknown", val) |
|
|
|
|
|
|
|
result, err := fc.GetActionResult(result_map, m.Service, m.Action, actArg) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
fmt.Println(err.Error()) |
|
|
|
collect_errors.Inc() |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
val, ok := result[m.Result] |
|
|
|
if !ok { |
|
|
|
fmt.Printf("%s.%s has no result %s", m.Service, m.Action, m.Result) |
|
|
|
collect_errors.Inc() |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
ch <- prometheus.MustNewConstMetric( |
|
|
|
m.Desc, |
|
|
|
m.MetricType, |
|
|
|
floatval, |
|
|
|
fc.Gateway, |
|
|
|
) |
|
|
|
fc.ReportMetric(ch, m, val) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -219,26 +302,20 @@ func test() { |
|
|
|
sort.Strings(actionKeys) |
|
|
|
for _, l := range actionKeys { |
|
|
|
a := s.Actions[l] |
|
|
|
|
|
|
|
if !a.IsGetOnly() { |
|
|
|
fmt.Printf(" %s - not calling - arguments: variable [direction] (soap name, soap type\n", a.Name) |
|
|
|
fmt.Printf(" %s - arguments: variable [direction] (soap name, soap type)\n", a.Name) |
|
|
|
for _, arg := range a.Arguments { |
|
|
|
sv := arg.StateVariable |
|
|
|
fmt.Printf(" %s [%s] (%s, %s)\n", arg.RelatedStateVariable, arg.Direction, arg.Name, sv.DataType) |
|
|
|
} |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf(" %s\n", a.Name) |
|
|
|
res, err := a.Call() |
|
|
|
if err != nil { |
|
|
|
fmt.Printf(" FAILED:%s\n", err.Error()) |
|
|
|
if !a.IsGetOnly() { |
|
|
|
fmt.Printf(" %s - not calling, since arguments required or no output\n", a.Name) |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
// only create JSON for Get
|
|
|
|
// TODO also create JSON templates for input actionParams
|
|
|
|
for _, arg := range a.Arguments { |
|
|
|
fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name]) |
|
|
|
|
|
|
|
// create new json entry
|
|
|
|
if(newEntry) { |
|
|
|
json.WriteString(",\n") |
|
|
@ -254,6 +331,18 @@ func test() { |
|
|
|
json.WriteString(arg.RelatedStateVariable) |
|
|
|
json.WriteString("\"\n\t}") |
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf(" %s - calling - results: variable: value\n", a.Name) |
|
|
|
res, err := a.Call(nil) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
fmt.Printf(" FAILED:%s\n", err.Error()) |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
for _, arg := range a.Arguments { |
|
|
|
fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name]) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -321,6 +410,24 @@ func main() { |
|
|
|
Password: *flag_gateway_password, |
|
|
|
} |
|
|
|
|
|
|
|
if *flag_collect { |
|
|
|
collector.LoadServices() |
|
|
|
|
|
|
|
prometheus.MustRegister(collector) |
|
|
|
prometheus.MustRegister(collect_errors) |
|
|
|
|
|
|
|
fmt.Println("collecting metrics via http") |
|
|
|
|
|
|
|
// simulate HTTP request without starting actual http server
|
|
|
|
writer := TestResponseWriter{header: http.Header{}} |
|
|
|
request := http.Request{} |
|
|
|
promhttp.Handler().ServeHTTP(&writer, &request) |
|
|
|
|
|
|
|
fmt.Println(writer.String()) |
|
|
|
|
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
go collector.LoadServices() |
|
|
|
|
|
|
|
prometheus.MustRegister(collector) |
|
|
@ -328,5 +435,6 @@ func main() { |
|
|
|
|
|
|
|
http.Handle("/metrics", promhttp.Handler()) |
|
|
|
fmt.Printf("metrics available at http://%s/metrics\n", *flag_addr) |
|
|
|
|
|
|
|
log.Fatal(http.ListenAndServe(*flag_addr, nil)) |
|
|
|
} |
|
|
|