From 693e1b82a96e1a61f4d27a079280dd35aed4ccca Mon Sep 17 00:00:00 2001 From: Nils Decker Date: Sun, 11 Dec 2022 00:13:47 +0100 Subject: [PATCH] split into smaller files and simplify connection parameter handling. --- collector.go | 133 ++++++++++++++++ fritzbox_upnp/action.go | 164 +++++++++++++++++++ fritzbox_upnp/service.go | 259 ++++++------------------------ go.mod | 2 +- main.go | 336 +++------------------------------------ metrics.go | 162 +++++++++++++++++++ 6 files changed, 528 insertions(+), 528 deletions(-) create mode 100644 collector.go create mode 100644 fritzbox_upnp/action.go create mode 100644 metrics.go diff --git a/collector.go b/collector.go new file mode 100644 index 0000000..997610b --- /dev/null +++ b/collector.go @@ -0,0 +1,133 @@ +package main + +import ( + "fmt" + "sync" + "time" + + upnp "github.com/ndecker/fritzbox_exporter/fritzbox_upnp" + "github.com/prometheus/client_golang/prometheus" +) + +const serviceLoadRetryTime = 1 * time.Minute + +var collectErrors = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "fritzbox_exporter_collect_errors", + Help: "Number of collection errors.", +}) + +type FritzboxCollector struct { + Parameters upnp.ConnectionParameters + sync.Mutex // protects Root + Root *upnp.Root +} + +// LoadServices tries to load the service information. Retries until success. +func (fc *FritzboxCollector) LoadServices() { + for { + root, err := upnp.LoadServices(fc.Parameters) + if err != nil { + fmt.Printf("cannot load services: %s\n", err) + + time.Sleep(serviceLoadRetryTime) + continue + } + + fmt.Printf("services loaded\n") + + fc.Lock() + fc.Root = root + fc.Unlock() + return + } +} + +func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { + for _, m := range metrics { + ch <- m.Desc + } +} + +func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { + fc.Lock() + root := fc.Root + fc.Unlock() + + if root == nil { + return // Services not loaded yet + } + + var err error + var lastService string + var lastMethod string + var lastResult upnp.Result + + for _, m := range metrics { + if m.Service != lastService || m.Action != lastMethod { + 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 + } + + lastResult, err = action.Call() + if err != nil { + fmt.Println(err) + collectErrors.Inc() + continue + } + } + + val, ok := lastResult[m.Result] + if !ok { + fmt.Println("result not found", m.Result) + collectErrors.Inc() + continue + } + + floatVal, ok := toFloat(val, m.OkValue) + if !ok { + fmt.Println("cannot convert to float:", val) + collectErrors.Inc() + continue + } + + ch <- prometheus.MustNewConstMetric( + m.Desc, + m.MetricType, + floatVal, + fc.Parameters.Device, + ) + } +} + +func toFloat(val any, okValue string) (float64, bool) { + switch tval := val.(type) { + case uint64: + return float64(tval), true + case bool: + if tval { + return 1, true + } else { + return 0, true + } + case string: + if tval == okValue { + return 1, true + } else { + return 0, true + } + default: + return 0, false + + } + +} diff --git a/fritzbox_upnp/action.go b/fritzbox_upnp/action.go new file mode 100644 index 0000000..1a14941 --- /dev/null +++ b/fritzbox_upnp/action.go @@ -0,0 +1,164 @@ +package fritzbox_upnp + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "log" + "net/http" + "strconv" + "strings" + + dac "github.com/123Haynes/go-http-digest-auth-client" +) + +// An UPNP Acton on a service +type Action struct { + service *Service + + Name string `xml:"name"` + Arguments []*Argument `xml:"argumentList>argument"` + ArgumentMap map[string]*Argument // Map of arguments indexed by .Name +} + +// IsGetOnly returns if the action seems to be a query for information. +// This is determined by checking if the action has no input arguments and at least one output argument. +func (a *Action) IsGetOnly() bool { + for _, a := range a.Arguments { + if a.Direction == "in" { + return false + } + } + return len(a.Arguments) > 0 +} + +// An Argument to an action +type Argument struct { + Name string `xml:"name"` + Direction string `xml:"direction"` + RelatedStateVariable string `xml:"relatedStateVariable"` + StateVariable *StateVariable +} + +// A state variable that can be manipulated through actions +type StateVariable struct { + Name string `xml:"name"` + DataType string `xml:"dataType"` + DefaultValue string `xml:"defaultValue"` +} + +// The result of a Call() contains all output arguments of the call. +// The map is indexed by the name of the state variable. +// The type of the value is string, uint64 or bool depending of the DataType of the variable. +type Result map[string]interface{} + +// Call an action. +// Currently only actions without input arguments are supported. +func (a *Action) Call() (Result, error) { + bodystr := fmt.Sprintf(` + + + + + + + `, a.Name, a.service.ServiceType) + + url := a.service.Device.root.baseUrl + a.service.ControlUrl + body := strings.NewReader(bodystr) + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + + action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) + + req.Header.Set("Content-Type", textXml) + req.Header.Set("SoapAction", action) + + t := dac.NewTransport(a.service.Device.root.params.Username, a.service.Device.root.params.Password) + + resp, err := t.RoundTrip(req) + if err != nil { + log.Fatalln(err) + } + defer closeIgnoringError(resp.Body) + + if resp.StatusCode == 401 { + return nil, fmt.Errorf("cannot read service %s: status 401 unauthorized", a.Name) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read request body: %w", err) + } + + return a.parseSoapResponse(data) + +} + +func (a *Action) parseSoapResponse(data []byte) (Result, error) { + res := make(Result) + dec := xml.NewDecoder(bytes.NewReader(data)) + + for { + t, err := dec.Token() + if err == io.EOF { + return res, nil + } + + if err != nil { + return nil, fmt.Errorf("cannot parse soap response: %w; body: %s", err, data) + } + + if se, ok := t.(xml.StartElement); ok { + arg, ok := a.ArgumentMap[se.Name.Local] + + if ok { + t2, err := dec.Token() + if err != nil { + return nil, err + } + + var val string + switch element := t2.(type) { + case xml.EndElement: + val = "" + case xml.CharData: + val = string(element) + default: + return nil, ErrInvalidSOAPResponse + } + + converted, err := convertResult(val, arg) + if err != nil { + return nil, err + } + res[arg.StateVariable.Name] = converted + } + } + + } +} + +func convertResult(val string, arg *Argument) (interface{}, error) { + switch arg.StateVariable.DataType { + case "string": + return val, nil + case "boolean": + return val == "1", nil + + case "ui1", "ui2", "ui4": + // type ui4 can contain values greater than 2^32! + res, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return nil, err + } + return res, nil + default: + return nil, fmt.Errorf("unknown datatype: %s", arg.StateVariable.DataType) + + } +} diff --git a/fritzbox_upnp/service.go b/fritzbox_upnp/service.go index d1ef353..137d188 100644 --- a/fritzbox_upnp/service.go +++ b/fritzbox_upnp/service.go @@ -21,12 +21,8 @@ import ( "errors" "fmt" "io" - "log" "net/http" - "strconv" - "strings" - - dac "github.com/123Haynes/go-http-digest-auth-client" + "net/url" ) // curl http://fritz.box:49000/igddesc.xml @@ -40,13 +36,24 @@ import ( const textXml = `text/xml; charset="utf-8"` +const ( + igdServiceDescriptor = "igddesc.xml" + tr64ServiceDescriptor = "tr64desc.xml" +) + var ErrInvalidSOAPResponse = errors.New("invalid SOAP response") -// Root of the UPNP tree -type Root struct { - BaseUrl string +type ConnectionParameters struct { + Device string // Hostname or IP + Port int Username string Password string +} + +// Root of the UPNP tree +type Root struct { + baseUrl string + params ConnectionParameters Device Device `xml:"device"` Services map[string]*Service // Map of all services indexed by .ServiceType } @@ -90,103 +97,6 @@ type scpdRoot struct { StateVariables []*StateVariable `xml:"serviceStateTable>stateVariable"` } -// An UPNP Acton on a service -type Action struct { - service *Service - - Name string `xml:"name"` - Arguments []*Argument `xml:"argumentList>argument"` - ArgumentMap map[string]*Argument // Map of arguments indexed by .Name -} - -// IsGetOnly returns if the action seems to be a query for information. -// This is determined by checking if the action has no input arguments and at least one output argument. -func (a *Action) IsGetOnly() bool { - for _, a := range a.Arguments { - if a.Direction == "in" { - return false - } - } - return len(a.Arguments) > 0 -} - -// An Argument to an action -type Argument struct { - Name string `xml:"name"` - Direction string `xml:"direction"` - RelatedStateVariable string `xml:"relatedStateVariable"` - StateVariable *StateVariable -} - -// A state variable that can be manipulated through actions -type StateVariable struct { - Name string `xml:"name"` - DataType string `xml:"dataType"` - DefaultValue string `xml:"defaultValue"` -} - -// The result of a Call() contains all output arguments of the call. -// The map is indexed by the name of the state variable. -// The type of the value is string, uint64 or bool depending of the DataType of the variable. -type Result map[string]interface{} - -// load the whole tree -func (r *Root) load() error { - igddesc, err := http.Get( - fmt.Sprintf("%s/igddesc.xml", r.BaseUrl), - ) - - if err != nil { - return err - } - defer closeIgnoringError(igddesc.Body) - - if igddesc.StatusCode == 404 { - return fmt.Errorf("http error 401 when loading service description. Is UPnP activated? (see Readme)") - } - - body, err := io.ReadAll(igddesc.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %w", err) - } - - dec := xml.NewDecoder(bytes.NewReader(body)) - - err = dec.Decode(r) - if err != nil { - return fmt.Errorf("failed to decode igdesc.xml: %w; body: %s", err, body) - } - - r.Services = make(map[string]*Service) - return r.Device.fillServices(r) -} - -func (r *Root) loadTr64() error { - igddesc, err := http.Get( - fmt.Sprintf("%s/tr64desc.xml", r.BaseUrl), - ) - - if err != nil { - return fmt.Errorf("failed to decode tr64desc.xml: %w", err) - } - - defer closeIgnoringError(igddesc.Body) - - if igddesc.StatusCode == 404 { - return fmt.Errorf("http error 401 when loading service description. Is UPnP activated? (see Readme)") - } - - dec := xml.NewDecoder(igddesc.Body) - - err = dec.Decode(r) - if err != nil { - return err - } - - r.Services = make(map[string]*Service) - return r.Device.fillServices(r) -} - // load all service descriptions func (d *Device) fillServices(r *Root) error { d.root = r @@ -194,7 +104,7 @@ func (d *Device) fillServices(r *Root) error { for _, s := range d.Services { s.Device = d - response, err := http.Get(r.BaseUrl + s.SCPDUrl) + response, err := http.Get(r.baseUrl + s.SCPDUrl) if err != nil { return err } @@ -239,141 +149,62 @@ func (d *Device) fillServices(r *Root) error { return nil } -// Call an action. -// Currently only actions without input arguments are supported. -func (a *Action) Call() (Result, error) { - bodystr := fmt.Sprintf(` - - - - - - - `, a.Name, a.service.ServiceType) - - url := a.service.Device.root.BaseUrl + a.service.ControlUrl - body := strings.NewReader(bodystr) - - req, err := http.NewRequest("POST", url, body) +// LoadServiceRoot loads a service descriptor and poplates a Service Root +func LoadServiceRoot(params ConnectionParameters, descriptor string) (*Root, error) { + var root = &Root{ + params: params, + baseUrl: fmt.Sprintf("http://%s:%d", params.Device, params.Port), + Services: make(map[string]*Service), + } + + descUrl, err := url.JoinPath(root.baseUrl, descriptor) if err != nil { return nil, err } - action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) - - req.Header.Set("Content-Type", textXml) - req.Header.Set("SoapAction", action) - - t := dac.NewTransport(a.service.Device.root.Username, a.service.Device.root.Password) - - resp, err := t.RoundTrip(req) + igddesc, err := http.Get(descUrl) if err != nil { - log.Fatalln(err) + return nil, err } - defer closeIgnoringError(resp.Body) + defer closeIgnoringError(igddesc.Body) - if resp.StatusCode == 401 { - return nil, fmt.Errorf("cannot read service %s: status 401 unauthorized", a.Name) + if igddesc.StatusCode == 404 { + return nil, fmt.Errorf("http error 401 when loading service description. Is UPnP activated? (see Readme)") } - data, err := io.ReadAll(resp.Body) + body, err := io.ReadAll(igddesc.Body) if err != nil { - return nil, fmt.Errorf("cannot read request body: %w", err) + return nil, fmt.Errorf("failed to read response body: %w", err) } - return a.parseSoapResponse(data) - -} - -func (a *Action) parseSoapResponse(data []byte) (Result, error) { - res := make(Result) - dec := xml.NewDecoder(bytes.NewReader(data)) - - for { - t, err := dec.Token() - if err == io.EOF { - return res, nil - } - - if err != nil { - return nil, fmt.Errorf("cannot parse soap response: %w; body: %s", err, data) - } - - if se, ok := t.(xml.StartElement); ok { - arg, ok := a.ArgumentMap[se.Name.Local] - - if ok { - t2, err := dec.Token() - if err != nil { - return nil, err - } - - var val string - switch element := t2.(type) { - case xml.EndElement: - val = "" - case xml.CharData: - val = string(element) - default: - return nil, ErrInvalidSOAPResponse - } - - converted, err := convertResult(val, arg) - if err != nil { - return nil, err - } - res[arg.StateVariable.Name] = converted - } - } + dec := xml.NewDecoder(bytes.NewReader(body)) + err = dec.Decode(root) + if err != nil { + return nil, fmt.Errorf("failed to decode igdesc.xml: %w; body: %s", err, body) } -} - -func convertResult(val string, arg *Argument) (interface{}, error) { - switch arg.StateVariable.DataType { - case "string": - return val, nil - case "boolean": - return val == "1", nil - - case "ui1", "ui2", "ui4": - // type ui4 can contain values greater than 2^32! - res, err := strconv.ParseUint(val, 10, 64) - if err != nil { - return nil, err - } - return res, nil - default: - return nil, fmt.Errorf("unknown datatype: %s", arg.StateVariable.DataType) + err = root.Device.fillServices(root) + if err != nil { + return nil, err } + + return root, nil } // LoadServices loads the services tree from a device. -func LoadServices(device string, port uint16, username string, password string) (*Root, error) { - var root = &Root{ - BaseUrl: fmt.Sprintf("http://%s:%d", device, port), - Username: username, - Password: password, - } - - err := root.load() +func LoadServices(params ConnectionParameters) (*Root, error) { + root, err := LoadServiceRoot(params, igdServiceDescriptor) if err != nil { return nil, err // already annotated } - var rootTr64 = &Root{ - BaseUrl: fmt.Sprintf("http://%s:%d", device, port), - Username: username, - Password: password, - } - - err = rootTr64.loadTr64() + rootTR64, err := LoadServiceRoot(params, tr64ServiceDescriptor) if err != nil { return nil, err // already annotated } - for k, v := range rootTr64.Services { + for k, v := range rootTR64.Services { root.Services[k] = v } diff --git a/go.mod b/go.mod index 90b904d..220a74d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ndecker/fritzbox_exporter -go 1.17 +go 1.18 require github.com/prometheus/client_golang v1.14.0 diff --git a/main.go b/main.go index ff697b1..4eaff6b 100644 --- a/main.go +++ b/main.go @@ -17,311 +17,45 @@ package main import ( "flag" "fmt" - "log" - "net/http" - "sync" - "time" - + upnp "github.com/ndecker/fritzbox_exporter/fritzbox_upnp" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - - upnp "github.com/ndecker/fritzbox_exporter/fritzbox_upnp" -) - -const serviceLoadRetryTime = 1 * time.Minute - -var ( - flagTest = flag.Bool("test", false, "print all available metrics to stdout") - flagAddr = flag.String("listen-address", ":9133", "The address to listen on for HTTP requests.") - - flagGatewayAddress = flag.String("gateway-address", "fritz.box", "The hostname or IP of the FRITZ!Box") - flagGatewayPort = flag.Int("gateway-port", 49000, "The port of the FRITZ!Box UPnP service") - flagGatewayUsername = flag.String("username", "", "The user for the FRITZ!Box UPnP service") - flagGatewayPassword = flag.String("password", "", "The password for the FRITZ!Box UPnP service") -) - -var ( - collectErrors = prometheus.NewCounter(prometheus.CounterOpts{ - Name: "fritzbox_exporter_collect_errors", - Help: "Number of collection errors.", - }) + "log" + "net/http" ) -type Metric struct { - Service string - Action string - Result string - OkValue string - - Desc *prometheus.Desc - MetricType prometheus.ValueType -} - -var metrics = []*Metric{ - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetTotalPacketsReceived", - Result: "TotalPacketsReceived", - Desc: prometheus.NewDesc( - "gateway_wan_packets_received", - "packets received on gateway WAN interface", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.CounterValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetTotalPacketsSent", - Result: "TotalPacketsSent", - Desc: prometheus.NewDesc( - "gateway_wan_packets_sent", - "packets sent on gateway WAN interface", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.CounterValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetAddonInfos", - Result: "TotalBytesReceived", - Desc: prometheus.NewDesc( - "gateway_wan_bytes_received", - "bytes received on gateway WAN interface", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.CounterValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetAddonInfos", - Result: "TotalBytesSent", - Desc: prometheus.NewDesc( - "gateway_wan_bytes_sent", - "bytes sent on gateway WAN interface", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.CounterValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetAddonInfos", - Result: "ByteSendRate", - Desc: prometheus.NewDesc( - "gateway_wan_bytes_send_rate", - "byte send rate on gateway WAN interface", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.GaugeValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetAddonInfos", - Result: "ByteReceiveRate", - Desc: prometheus.NewDesc( - "gateway_wan_bytes_receive_rate", - "byte receive rate on gateway WAN interface", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.GaugeValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetCommonLinkProperties", - Result: "Layer1UpstreamMaxBitRate", - Desc: prometheus.NewDesc( - "gateway_wan_layer1_upstream_max_bitrate", - "Layer1 upstream max bitrate", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.GaugeValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetCommonLinkProperties", - Result: "Layer1DownstreamMaxBitRate", - Desc: prometheus.NewDesc( - "gateway_wan_layer1_downstream_max_bitrate", - "Layer1 downstream max bitrate", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.GaugeValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", - Action: "GetCommonLinkProperties", - Result: "PhysicalLinkStatus", - OkValue: "Up", - Desc: prometheus.NewDesc( - "gateway_wan_layer1_link_status", - "Status of physical link (Up = 1)", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.GaugeValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANIPConnection:1", - Action: "GetStatusInfo", - Result: "ConnectionStatus", - OkValue: "Connected", - Desc: prometheus.NewDesc( - "gateway_wan_connection_status", - "WAN connection status (Connected = 1)", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.GaugeValue, - }, - { - Service: "urn:schemas-upnp-org:service:WANIPConnection:1", - Action: "GetStatusInfo", - Result: "Uptime", - Desc: prometheus.NewDesc( - "gateway_wan_connection_uptime_seconds", - "WAN connection uptime", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.GaugeValue, - }, - { - Service: "urn:dslforum-org:service:WLANConfiguration:1", - Action: "GetTotalAssociations", - Result: "TotalAssociations", - Desc: prometheus.NewDesc( - "gateway_wlan_current_connections", - "current WLAN connections", - []string{"gateway"}, - nil, - ), - MetricType: prometheus.GaugeValue, - }, -} - -type FritzboxCollector struct { - Gateway string - Port uint16 - Username string - Password string - - sync.Mutex // protects Root - Root *upnp.Root -} - -// LoadServices tries to load the service information. Retries until success. -func (fc *FritzboxCollector) LoadServices() { - for { - root, err := upnp.LoadServices(fc.Gateway, fc.Port, fc.Username, fc.Password) - if err != nil { - fmt.Printf("cannot load services: %s\n", err) +func main() { + flagTest := flag.Bool("test", false, "print all available metrics to stdout") + flagAddr := flag.String("listen-address", ":9133", "The address to listen on for HTTP requests.") - time.Sleep(serviceLoadRetryTime) - continue - } + var parameters upnp.ConnectionParameters + flag.StringVar(¶meters.Device, "gateway-address", "fritz.box", "The hostname or IP of the FRITZ!Box") + flag.IntVar(¶meters.Port, "gateway-port", 49000, "The port of the FRITZ!Box UPnP service") + flag.StringVar(¶meters.Username, "username", "", "The user for the FRITZ!Box UPnP service") + flag.StringVar(¶meters.Password, "password", "", "The password for the FRITZ!Box UPnP service") - fmt.Printf("services loaded\n") + flag.Parse() - fc.Lock() - fc.Root = root - fc.Unlock() + if *flagTest { + test(parameters) return } -} - -func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { - for _, m := range metrics { - ch <- m.Desc - } -} - -func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { - fc.Lock() - root := fc.Root - fc.Unlock() - if root == nil { - // Services not loaded yet - return + collector := &FritzboxCollector{ + Parameters: parameters, } - var err error - var lastService string - var lastMethod string - var lastResult upnp.Result - - for _, m := range metrics { - if m.Service != lastService || m.Action != lastMethod { - 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 - } - - lastResult, err = action.Call() - if err != nil { - fmt.Println(err) - collectErrors.Inc() - continue - } - } - - val, ok := lastResult[m.Result] - if !ok { - fmt.Println("result not found", m.Result) - collectErrors.Inc() - continue - } - - 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", val) - collectErrors.Inc() - continue + go collector.LoadServices() - } + prometheus.MustRegister(collector) + prometheus.MustRegister(collectErrors) - ch <- prometheus.MustNewConstMetric( - m.Desc, - m.MetricType, - floatval, - fc.Gateway, - ) - } + http.Handle("/metrics", promhttp.Handler()) + log.Fatal(http.ListenAndServe(*flagAddr, nil)) } -func test() { - root, err := upnp.LoadServices(*flagGatewayAddress, uint16(*flagGatewayPort), *flagGatewayUsername, *flagGatewayPassword) +func test(p upnp.ConnectionParameters) { + root, err := upnp.LoadServices(p) if err != nil { panic(err) } @@ -345,27 +79,3 @@ func test() { } } } - -func main() { - flag.Parse() - - if *flagTest { - test() - return - } - - collector := &FritzboxCollector{ - Gateway: *flagGatewayAddress, - Port: uint16(*flagGatewayPort), - Username: *flagGatewayUsername, - Password: *flagGatewayPassword, - } - - go collector.LoadServices() - - prometheus.MustRegister(collector) - prometheus.MustRegister(collectErrors) - - http.Handle("/metrics", promhttp.Handler()) - log.Fatal(http.ListenAndServe(*flagAddr, nil)) -} diff --git a/metrics.go b/metrics.go new file mode 100644 index 0000000..176f629 --- /dev/null +++ b/metrics.go @@ -0,0 +1,162 @@ +package main + +import "github.com/prometheus/client_golang/prometheus" + +type Metric struct { + Service string + Action string + Result string + OkValue string + + Desc *prometheus.Desc + MetricType prometheus.ValueType +} + +var metrics = []*Metric{ + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetTotalPacketsReceived", + Result: "TotalPacketsReceived", + Desc: prometheus.NewDesc( + "gateway_wan_packets_received", + "packets received on gateway WAN interface", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.CounterValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetTotalPacketsSent", + Result: "TotalPacketsSent", + Desc: prometheus.NewDesc( + "gateway_wan_packets_sent", + "packets sent on gateway WAN interface", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.CounterValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetAddonInfos", + Result: "TotalBytesReceived", + Desc: prometheus.NewDesc( + "gateway_wan_bytes_received", + "bytes received on gateway WAN interface", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.CounterValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetAddonInfos", + Result: "TotalBytesSent", + Desc: prometheus.NewDesc( + "gateway_wan_bytes_sent", + "bytes sent on gateway WAN interface", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.CounterValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetAddonInfos", + Result: "ByteSendRate", + Desc: prometheus.NewDesc( + "gateway_wan_bytes_send_rate", + "byte send rate on gateway WAN interface", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.GaugeValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetAddonInfos", + Result: "ByteReceiveRate", + Desc: prometheus.NewDesc( + "gateway_wan_bytes_receive_rate", + "byte receive rate on gateway WAN interface", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.GaugeValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetCommonLinkProperties", + Result: "Layer1UpstreamMaxBitRate", + Desc: prometheus.NewDesc( + "gateway_wan_layer1_upstream_max_bitrate", + "Layer1 upstream max bitrate", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.GaugeValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetCommonLinkProperties", + Result: "Layer1DownstreamMaxBitRate", + Desc: prometheus.NewDesc( + "gateway_wan_layer1_downstream_max_bitrate", + "Layer1 downstream max bitrate", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.GaugeValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1", + Action: "GetCommonLinkProperties", + Result: "PhysicalLinkStatus", + OkValue: "Up", + Desc: prometheus.NewDesc( + "gateway_wan_layer1_link_status", + "Status of physical link (Up = 1)", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.GaugeValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANIPConnection:1", + Action: "GetStatusInfo", + Result: "ConnectionStatus", + OkValue: "Connected", + Desc: prometheus.NewDesc( + "gateway_wan_connection_status", + "WAN connection status (Connected = 1)", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.GaugeValue, + }, + { + Service: "urn:schemas-upnp-org:service:WANIPConnection:1", + Action: "GetStatusInfo", + Result: "Uptime", + Desc: prometheus.NewDesc( + "gateway_wan_connection_uptime_seconds", + "WAN connection uptime", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.GaugeValue, + }, + { + Service: "urn:dslforum-org:service:WLANConfiguration:1", + Action: "GetTotalAssociations", + Result: "TotalAssociations", + Desc: prometheus.NewDesc( + "gateway_wlan_current_connections", + "current WLAN connections", + []string{"gateway"}, + nil, + ), + MetricType: prometheus.GaugeValue, + }, +}