diff --git a/README.md b/README.md index 6a1dbf8..166e507 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ -# Fritz!Box Upnp statistics exporter for prometheus +# FRITZ!Box Upnp statistics exporter for prometheus -This exporter exports some variables from an -[AVM Fritzbox](http://avm.de/produkte/fritzbox/) -to prometheus. +This exporter exports some variables from an [AVM Fritzbox](http://avm.de/produkte/fritzbox/) to prometheus. ## Compatibility @@ -35,27 +33,9 @@ This exporter is known to work with the following models: git clone https://github.com/ndecker/fritzbox_exporter/ docker build -t fritzbox_exporter fritzbox_exporter -## Running - -In the configuration of the Fritzbox the option "Statusinformationen über UPnP übertragen" in the dialog "Heimnetz > -Heimnetzübersicht > Netzwerkeinstellungen" has to be enabled. - -## Configuration - -| command line parameter | environment variable | default | | -|------------------------|---------------------------|-----------|------------------------------------------------| -| -listen-address | FRITZBOX_EXPORTER_LISTEN | :9133 | The address to listen on for HTTP requests | -| -gateway-address | FRITZBOX_DEVICE | fritz.box | The hostname or IP of the FRITZ!Box | -| -gateway-port | FRITZBOX_PORT | 49000 | The port of the FRITZ!Box UPnP service | -| -gateway-port | FRITZBOX_PORT_TLS | 49443 | The port of the FRITZ!Box TLS UPnP service | -| -username | FRITZBOX_USERNAME | | The user for the FRITZ!Box UPnP service | -| -password | FRITZBOX_PASSWORD | | The password for the FRITZ!Box UPnP service | -| -use-tls | FRITZBOX_USE_TLS | true | Use TLS/HTTPS connection to FRITZ!Box | -| -allow-selfsigned | FTITZBOX_ALLOW_SELFSIGNED | true | Allow selfsigned certificate from FRITZ!Box | -| -test | | | Print all available metrics to stdout and exit | - - ## Prerequisites +In the configuration of the Fritzbox the option "Statusinformationen über UPnP übertragen" has to be enabled. + ### FRITZ!OS 7.00+ `Heimnetz` > `Netzwerk` > `Netwerkeinstellungen` > `Statusinformationen über UPnP übertragen` @@ -64,190 +44,81 @@ Heimnetzübersicht > Netzwerkeinstellungen" has to be enabled. `Heimnetz` > `Heimnetzübersicht` > `Netzwerkeinstellungen` > `Statusinformationen über UPnP übertragen` + +## Configuration + +| command line parameter | environment variable | default | | +|------------------------|---------------------------|------------|------------------------------------------------------------| +| -metrics | FRITZBOX_EXPORTER_METRICS | | YAML file describing exported metrics | +| -test-metrics | | | Test which metrics can be read and print YAML metrics file | +| -listen-address | FRITZBOX_EXPORTER_LISTEN | :9133 | The address to listen on for HTTP requests | +| -gateway-address | FRITZBOX_DEVICE | fritz.box | The hostname or IP of the FRITZ!Box | +| -gateway-port | FRITZBOX_PORT | 49000 | The port of the FRITZ!Box UPnP service | +| -gateway-port | FRITZBOX_PORT_TLS | 49443 | The port of the FRITZ!Box TLS UPnP service | +| -username | FRITZBOX_USERNAME | | The user for the FRITZ!Box UPnP service | +| -password | FRITZBOX_PASSWORD | | The password for the FRITZ!Box UPnP service | +| -use-tls | FRITZBOX_USE_TLS | true | Use TLS/HTTPS connection to FRITZ!Box | +| -allow-selfsigned | FRITZBOX_ALLOW_SELFSIGNED | true | Allow selfsigned certificate from FRITZ!Box | + + ## Exported metrics -These metrics are exported: - - # HELP fritzbox_exporter_collect_errors Number of collection errors. - # TYPE fritzbox_exporter_collect_errors counter - fritzbox_exporter_collect_errors 0 - # HELP gateway_wan_bytes_received bytes received on gateway WAN interface - # TYPE gateway_wan_bytes_received counter - gateway_wan_bytes_received{gateway="fritz.box"} 5.037749914e+09 - # HELP gateway_wan_bytes_sent bytes sent on gateway WAN interface - # TYPE gateway_wan_bytes_sent counter - gateway_wan_bytes_sent{gateway="fritz.box"} 2.55707479e+08 - # HELP gateway_wan_connection_status WAN connection status (Connected = 1) - # TYPE gateway_wan_connection_status gauge - gateway_wan_connection_status{gateway="fritz.box"} 1 - # HELP gateway_wan_connection_uptime_seconds WAN connection uptime - # TYPE gateway_wan_connection_uptime_seconds gauge - gateway_wan_connection_uptime_seconds{gateway="fritz.box"} 65259 - # HELP gateway_wan_layer1_downstream_max_bitrate Layer1 downstream max bitrate - # TYPE gateway_wan_layer1_downstream_max_bitrate gauge - gateway_wan_layer1_downstream_max_bitrate{gateway="fritz.box"} 1.286e+07 - # HELP gateway_wan_layer1_link_status Status of physical link (Up = 1) - # TYPE gateway_wan_layer1_link_status gauge - gateway_wan_layer1_link_status{gateway="fritz.box"} 1 - # HELP gateway_wan_layer1_upstream_max_bitrate Layer1 upstream max bitrate - # TYPE gateway_wan_layer1_upstream_max_bitrate gauge - gateway_wan_layer1_upstream_max_bitrate{gateway="fritz.box"} 1.148e+06 - # HELP gateway_wan_packets_received packets received on gateway WAN interface - # TYPE gateway_wan_packets_received counter - gateway_wan_packets_received{gateway="fritz.box"} 1.346625e+06 - # HELP gateway_wan_packets_sent packets sent on gateway WAN interface - # TYPE gateway_wan_packets_sent counter - gateway_wan_packets_sent{gateway="fritz.box"} 3.05051e+06 - - -## Output of -test - -The exporter prints all available Variables to stdout when called with the -test option. -These values are determined by parsing all services from http://fritz.box:49000/igddesc.xml - - GetFirewallStatus - FirewallEnabled: true - InboundPinholeAllowed: false - GetInfo - MaxCharsPassword: 32 - MinCharsPassword: 0 - AllowedCharsPassword: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ - X_AVM-DE_GetAnonymousLogin - X_AVM-DE_AnonymousLoginEnabled: false - X_AVM-DE_ButtonLoginEnabled: false - X_AVM-DE_GetUserList - X_AVM-DE_UserList: xxx - X_AVM-DE_GetWLANConnectionInfo - AssociatedDeviceMACAddress: - SSID: - BSSID: - BeaconType: - Channel: - Standard: - X_AVM-DE_SignalStrength: - X_AVM-DE_Speed: - X_AVM-DE_SpeedRX: - X_AVM-DE_SpeedMax: - X_AVM-DE_SpeedRXMax: - GetHostNumberOfEntries - HostNumberOfEntries: 5 - X_AVM-DE_GetChangeCounter - X_AVM-DE_ChangeCounter: 0 - X_AVM-DE_DoUpdate - UpgradeAvailable: false - X_AVM-DE_UpdateState: NoUpdate - GetSecurityPort - SecurityPort: 49443 - GetAddonInfos - ByteSendRate: 35 - ByteReceiveRate: 26 - PacketSendRate: 0 - PacketReceiveRate: 0 - TotalBytesSent: 354858561 - TotalBytesReceived: 626712195 - AutoDisconnectTime: 0 - IdleDisconnectTime: 1 - DNSServer1: xxx.xxx.xxx.xxx - DNSServer2: xxx.xxx.xxx.xxx - VoipDNSServer1: xxx.xxx.xxx.xxx - VoipDNSServer2: xxx.xxx.xxx.xxx - UpnpControlEnabled: false - RoutedBridgedModeBoth: 1 - X_AVM_DE_TotalBytesSent64: 354858561 - X_AVM_DE_TotalBytesReceived64: 4921679491 - X_AVM_DE_WANAccessType: DSL - X_AVM_DE_GetDsliteStatus - X_AVM_DE_DsliteStatus: false - X_AVM_DE_GetIPTVInfos - X_AVM_DE_IPTV_Enabled: false - X_AVM_DE_IPTV_Provider: - X_AVM_DE_IPTV_URL: - GetCommonLinkProperties - WANAccessType: DSL - Layer1UpstreamMaxBitRate: 33251000 - Layer1DownstreamMaxBitRate: 114110000 - PhysicalLinkStatus: Up - GetTotalBytesSent - TotalBytesSent: 354858561 - GetTotalBytesReceived - TotalBytesReceived: 626712195 - GetTotalPacketsSent - TotalPacketsSent: 245896 - GetTotalPacketsReceived - TotalPacketsReceived: 69248 - X_AVM_DE_GetDNSServer - IPv4DNSServer1: xxx.xxx.xxx.xxx - IPv4DNSServer2: xxx.xxx.xxx.xxx - GetIdleDisconnectTime - IdleDisconnectTime: 0 - GetStatusInfo - ConnectionStatus: Connected - LastConnectionError: ERROR_NONE - Uptime: 70993 - GetNATRSIPStatus - RSIPAvailable: false - NATEnabled: true - GetAutoDisconnectTime - AutoDisconnectTime: 0 - X_AVM_DE_GetIPv6Prefix - IPv6Prefix: - PrefixLength: 0 - ValidLifetime: 0 - PreferedLifetime: 0 - GetConnectionTypeInfo - ConnectionType: IP_Routed - PossibleConnectionTypes: IP_Routed - X_AVM_DE_GetExternalIPv6Address - ExternalIPv6Address: - PrefixLength: 0 - ValidLifetime: 0 - PreferedLifetime: 0 - X_AVM_DE_GetIPv6DNSServer - IPv6DNSServer1: - ValidLifetime1: 0 - IPv6DNSServer2: - ValidLifetime2: 2003335812 - GetExternalIPAddress - ExternalIPAddress: xxx.xxx.xxx.xxx - X_AVM-DE_GetNightControl - NightControl: - - NightTimeControlNoForcedOff: false - X_AVM-DE_GetWLANConnectionInfo - AssociatedDeviceMACAddress: - SSID: - BSSID: - BeaconType: - Channel: - Standard: - X_AVM-DE_SignalStrength: - X_AVM-DE_Speed: - X_AVM-DE_SpeedRX: - X_AVM-DE_SpeedMax: - X_AVM-DE_SpeedRXMax: - X_AVM-DE_GetWLANConnectionInfo - AssociatedDeviceMACAddress: - SSID: - BSSID: - BeaconType: - Channel: - Standard: - X_AVM-DE_SignalStrength: - X_AVM-DE_Speed: - X_AVM-DE_SpeedRX: - X_AVM-DE_SpeedMax: - X_AVM-DE_SpeedRXMax: - GetAutoConfig - AutoConfig: false - GetModulationType - ModulationType: - GetDSLLinkInfo - LinkType: PPPoE - LinkStatus: Up - GetATMEncapsulation - ATMEncapsulation: - GetFCSPreserved - FCSPreserved: false - GetDestinationAddress - DestinationAddress: +The default metrics to be exported are described in [default-metrics.yaml](default-metrics.yaml). +This file is compiled into the binary and used by default. +With the `-metrics` option a different file can be specified. + +With the `-test-metrics` option all possible metrics of the FRITZ!Box can be queried. This can take a few minutes. +For TR64 metrics username/password must be provided. + + fritzbox_exporter -test-metrics > metrics.yaml + edit metrics.yaml + fritzbox_exporter -metrics metrics.yaml + +### Examples + +This is an example metric as exported by `-test-metrics` + + - metric: "" # prometheus metric name (required) + help: "" # prometheus help text + type: "" # metric type: gauge, counter + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetTotalBytesReceived + result: TotalBytesReceived + examplevalue: "325538505" # current value of the metric. Only for info; not used + source: "igddesc.xml" # source of the value (iggdesc.xml or tr64desc.xml). Only for info; not used + + +If you wanted to for example to monitor the number of hosts in you local network, you could use this: + + - metric: "gateway_number_of_hosts" + help: "Number of hosts in the local network" + type: "gauge" + service: urn:dslforum-org:service:Hosts:1 + action: GetHostNumberOfEntries + result: HostNumberOfEntries + +### Metrics with `okvalue` + +If the value is a string you can use the `okvalue` field to specify a value to compare the string to. +The metric will be 1 if the value matches okvalue; 0 otherwise. + + - metric: gateway_wan_layer1_link_status + help: Status of physical link (Up = 1) + type: gauge + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetCommonLinkProperties + result: PhysicalLinkStatus + okvalue: Up + +### Metrics with `labelname` + +You can specify `labelname` to set a metric label with the value. The metric value will always be 1. +The following will give a metric `gateway_version{device="fritz.box", version="113.07.29"} = 1` + + - metric: "gateway_version" + type: "gauge" + service: urn:dslforum-org:service:DeviceInfo:1 + action: GetInfo + result: SoftwareVersion + labelname: version + source: tr64desc.xml diff --git a/collector.go b/collector.go index c70d5fe..f807856 100644 --- a/collector.go +++ b/collector.go @@ -12,24 +12,58 @@ import ( const serviceLoadRetryTime = 1 * time.Minute -var collectErrors = prometheus.NewCounter(prometheus.CounterOpts{ - Name: "fritzbox_exporter_collect_errors", - Help: "Number of collection errors.", -}) +var ( + numCalls = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "fritzbox_exporter_calls", + Help: "Number of calls to a service action.", + }) + + collectErrors = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "fritzbox_exporter_collect_errors", + Help: "Number of collection errors.", + }) + serviceNotFound = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "fritzbox_exporter_service_not_found", + Help: "", + }, []string{"service"}) + actionNotFound = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "fritzbox_exporter_action_not_found", + Help: "", + }, []string{"action"}) + resultNotFound = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "fritzbox_exporter_result_not_found", + Help: "", + }, []string{"result"}) + + collectMetrics = []prometheus.Collector{numCalls, collectErrors, serviceNotFound, actionNotFound, resultNotFound} +) type FritzboxCollector struct { Parameters upnp.ConnectionParameters - sync.Mutex // protects Roots - IGDRoot *upnp.Root - TR64Root *upnp.Root + Metrics []*Metric + + sync.RWMutex // protects services + services map[string]*upnp.Service +} + +func NewCollector(params upnp.ConnectionParameters, metrics []*Metric) *FritzboxCollector { + c := &FritzboxCollector{ + Parameters: params, + Metrics: metrics, + services: make(map[string]*upnp.Service), + } + go c.loadServices() + return c } // LoadServices tries to load the service information. Retries until success. -func (fc *FritzboxCollector) LoadServices() { +func (fc *FritzboxCollector) loadServices() { igdRoot := fc.loadService(upnp.IGDServiceDescriptor) log.Printf("%d IGD services loaded\n", len(igdRoot.Services)) fc.Lock() - fc.IGDRoot = igdRoot + for _, s := range igdRoot.Services { + fc.services[s.ServiceType] = s + } fc.Unlock() if fc.Parameters.Username == "" { @@ -39,7 +73,9 @@ func (fc *FritzboxCollector) LoadServices() { tr64Root := fc.loadService(upnp.TR64ServiceDescriptor) log.Printf("%d TR64 services loaded\n", len(tr64Root.Services)) fc.Lock() - fc.TR64Root = tr64Root + for _, s := range tr64Root.Services { + fc.services[s.ServiceType] = s + } fc.Unlock() } @@ -47,6 +83,7 @@ func (fc *FritzboxCollector) loadService(desc string) *upnp.Root { for { root, err := upnp.LoadServiceRoot(fc.Parameters, desc) if err != nil { + collectErrors.Inc() fmt.Printf("cannot load services: %s\n", err) time.Sleep(serviceLoadRetryTime) @@ -57,95 +94,104 @@ func (fc *FritzboxCollector) loadService(desc string) *upnp.Root { } func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { - for _, m := range IGDMetrics { - ch <- m.Desc - } - for _, m := range TR64Metrics { - ch <- m.Desc + for _, m := range fc.Metrics { + ch <- m.desc } } func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { - fc.Lock() - root := fc.IGDRoot - fc.Unlock() - fc.collectRoot(ch, root, IGDMetrics) - - fc.Lock() - root = fc.TR64Root - fc.Unlock() - fc.collectRoot(ch, root, TR64Metrics) -} + fc.RLock() + defer fc.RUnlock() -func (fc *FritzboxCollector) collectRoot(ch chan<- prometheus.Metric, root *upnp.Root, metrics []*Metric) { - if root == nil { - return // Services not loaded yet + type cacheKey struct { + Service string + Action string } - var err error - var lastService string - var lastMethod string - var lastResult upnp.Result + // Cache Action call result. Multiple metrics might use different results from a call. + resultCache := make(map[cacheKey]upnp.Result) - for _, m := range metrics { - if m.Service != lastService || m.Action != lastMethod { - service, ok := root.Services[m.Service] + for _, m := range fc.Metrics { + result, ok := resultCache[cacheKey{ + Service: m.Service, + Action: m.Action, + }] + + if !ok { + service, ok := fc.services[m.Service] if !ok { - // TODO - fmt.Println("cannot find service", m.Service) - fmt.Println(root.Services) + serviceNotFound.WithLabelValues(m.Service).Inc() continue } action, ok := service.Actions[m.Action] if !ok { - // TODO - fmt.Println("cannot find action", m.Action) + actionNotFound.WithLabelValues(m.Action).Inc() continue } - lastResult, err = action.Call() + numCalls.Inc() + var err error + result, err = action.Call() if err != nil { fmt.Println(err) collectErrors.Inc() continue } + + resultCache[cacheKey{ + Service: m.Service, + Action: m.Action, + }] = result } - val, ok := lastResult[m.Result] + val, ok := result[m.Result] if !ok { - fmt.Println("result not found", m.Result) - collectErrors.Inc() + resultNotFound.WithLabelValues(m.Result).Inc() continue } + fc.exportMetric(m, ch, val) + } +} + +func (fc *FritzboxCollector) exportMetric(m *Metric, ch chan<- prometheus.Metric, val interface{}) { + if m.LabelName == "" { + // normal metric + floatVal, ok := toFloat(val, m.OkValue) if !ok { - fmt.Println("cannot convert to float:", val) + log.Println("cannot convert to float:", val) collectErrors.Inc() - continue } ch <- prometheus.MustNewConstMetric( - m.Desc, - m.MetricType, - floatVal, + m.desc, m.metricType, floatVal, fc.Parameters.Device, ) + } else { + // value as label metric + stringVal := fmt.Sprintf("%s", val) + ch <- prometheus.MustNewConstMetric( + m.desc, m.metricType, 1.0, + fc.Parameters.Device, stringVal, + ) } } func toFloat(val any, okValue string) (float64, bool) { - switch tval := val.(type) { + switch val := val.(type) { case uint64: - return float64(tval), true + return float64(val), true case bool: - if tval { + if val { return 1, true } else { return 0, true } case string: - if tval == okValue { + if okValue == "" { + return 0, false + } else if val == okValue { return 1, true } else { return 0, true diff --git a/default-metrics.yaml b/default-metrics.yaml new file mode 100644 index 0000000..66c1392 --- /dev/null +++ b/default-metrics.yaml @@ -0,0 +1,80 @@ +- metric: gateway_wan_packets_received + help: packets received on gateway WAN interface + type: counter + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetTotalPacketsReceived + result: TotalPacketsReceived +- metric: gateway_wan_packets_sent + help: packets sent on gateway WAN interface + type: counter + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetTotalPacketsSent + result: TotalPacketsSent +- metric: gateway_wan_bytes_received + help: bytes received on gateway WAN interface + type: counter + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetAddonInfos + result: TotalBytesReceived +- metric: gateway_wan_bytes_sent + help: bytes sent on gateway WAN interface + type: counter + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetAddonInfos + result: TotalBytesSent +- metric: gateway_wan_bytes_send_rate + help: byte send rate on gateway WAN interface + type: gauge + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetAddonInfos + result: ByteSendRate +- metric: gateway_wan_bytes_receive_rate + help: byte receive rate on gateway WAN interface + type: gauge + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetAddonInfos + result: ByteReceiveRate +- metric: gateway_wan_layer1_upstream_max_bitrate + help: Layer1 upstream max bitrate + type: gauge + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetCommonLinkProperties + result: Layer1UpstreamMaxBitRate +- metric: gateway_wan_layer1_downstream_max_bitrate + help: Layer1 downstream max bitrate + type: gauge + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetCommonLinkProperties + result: Layer1DownstreamMaxBitRate +- metric: gateway_wan_layer1_link_status + help: Status of physical link (Up = 1) + type: gauge + service: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + action: GetCommonLinkProperties + result: PhysicalLinkStatus + okvalue: Up +- metric: gateway_wan_connection_status + help: WAN connection status (Connected = 1) + type: gauge + service: urn:schemas-upnp-org:service:WANIPConnection:1 + action: GetStatusInfo + result: ConnectionStatus + okvalue: Connected +- metric: gateway_wan_connection_uptime_seconds + help: WAN connection uptime + type: gauge + service: urn:schemas-upnp-org:service:WANIPConnection:1 + action: GetStatusInfo + result: Uptime +- metric: gateway_wlan_current_connections + help: current WLAN connections + type: gauge + service: urn:dslforum-org:service:WLANConfiguration:1 + action: GetTotalAssociations + result: TotalAssociations +- metric: "gateway_version" + type: "gauge" + service: urn:dslforum-org:service:DeviceInfo:1 + action: GetInfo + result: SoftwareVersion + labelname: version diff --git a/go.mod b/go.mod index fc98a71..fc9f034 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,11 @@ go 1.18 require ( github.com/ndecker/go-http-digest-auth-client v0.4.0 github.com/prometheus/client_golang v1.14.0 + gopkg.in/yaml.v3 v3.0.1 ) +// replace github.com/ndecker/go-http-digest-auth-client => ../go-http-digest-auth-client + require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect diff --git a/go.sum b/go.sum index e08a057..2affac4 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -460,6 +462,7 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -468,6 +471,8 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 9df354a..c97ae3d 100644 --- a/main.go +++ b/main.go @@ -16,31 +16,40 @@ package main import ( "flag" - "fmt" - upnp "github.com/ndecker/fritzbox_exporter/fritzbox_upnp" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" + "io" "log" "net/http" "os" "strconv" + + upnp "github.com/ndecker/fritzbox_exporter/fritzbox_upnp" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" ) func main() { - listenAddress := GetEnv("FRITZBOX_EXPORTER_LISTEN", ":9133") + err := run() + if err != nil { + log.Fatal(err) + } +} + +func run() error { + listenAddress := getEnv("FRITZBOX_EXPORTER_LISTEN", ":9133") flag.StringVar(&listenAddress, "listen-address", listenAddress, "The address to listen on for HTTP requests.") - flagTestIGD := flag.Bool("test-igd", false, "print all available IGD metrics to stdout") - flagTestTR64 := flag.Bool("test-tr64", false, "print all available TR64 metrics to stdout") + flagMetricsYamlFile := flag.String("metrics", os.Getenv("FRITZBOX_EXPORTER_METRICS"), "YAML file for metrics") + + flagTest := flag.Bool("test-metrics", false, "Test which metrics can be read and print YAML metrics file") parameters := upnp.ConnectionParameters{ - Device: GetEnv("FRITZBOX_DEVICE", "fritz.box"), - Port: GetEnvInt("FRITZBOX_PORT", 49000), - PortTLS: GetEnvInt("FRITZBOX_PORT_TLS", 49443), - Username: GetEnv("FRITZBOX_USERNAME", ""), - Password: GetEnv("FRITZBOX_PASSWORD", ""), - UseTLS: GetEnv("FRITZBOX_USE_TLS", "true") == "true", - AllowSelfSigned: GetEnv("FRITZBOX_ALLOW_SELFSIGNED", "true") == "true", + Device: getEnv("FRITZBOX_DEVICE", "fritz.box"), + Port: getEnvInt("FRITZBOX_PORT", 49000), + PortTLS: getEnvInt("FRITZBOX_PORT_TLS", 49443), + Username: getEnv("FRITZBOX_USERNAME", ""), + Password: getEnv("FRITZBOX_PASSWORD", ""), + UseTLS: getEnv("FRITZBOX_USE_TLS", "true") == "true", + AllowSelfSigned: getEnv("FRITZBOX_ALLOW_SELFSIGNED", "true") == "true", } flag.StringVar(¶meters.Device, "gateway-address", parameters.Device, "The hostname or IP of the FRITZ!Box") @@ -53,59 +62,55 @@ func main() { flag.Parse() - if *flagTestIGD { - test(parameters, upnp.IGDServiceDescriptor) - return - } - if *flagTestTR64 { + if *flagTest { + err := testMetrics(parameters, upnp.IGDServiceDescriptor) + if err != nil { + return err + } + if parameters.Username == "" { log.Fatal("no username/password set for TR64") } - test(parameters, upnp.TR64ServiceDescriptor) - return + err = testMetrics(parameters, upnp.TR64ServiceDescriptor) + if err != nil { + return err + } + return nil } - collector := &FritzboxCollector{ - Parameters: parameters, + var metricsYaml []byte + if *flagMetricsYamlFile == "" { + metricsYaml = defaultMetricsYaml + } else { + f, err := os.Open(*flagMetricsYamlFile) + if err != nil { + return err + } + + metricsYaml, err = io.ReadAll(f) + if err != nil { + return err + } + _ = f.Close() } - go collector.LoadServices() + metrics, err := loadMetrics(metricsYaml) + if err != nil { + return err + } - prometheus.MustRegister(collector) - prometheus.MustRegister(collectErrors) + log.Printf("loaded %d metrics", len(metrics)) - http.Handle("/metrics", promhttp.Handler()) - log.Fatal(http.ListenAndServe(listenAddress, nil)) -} + collector := NewCollector(parameters, metrics) -func test(p upnp.ConnectionParameters, desc string) { - root, err := upnp.LoadServiceRoot(p, desc) - if err != nil { - panic(err) - } + prometheus.MustRegister(collector) + prometheus.MustRegister(collectMetrics...) - for _, s := range root.Services { - fmt.Println(s.SCPDUrl) - for _, a := range s.Actions { - if !a.IsGetOnly() { - continue - } - - res, err := a.Call() - if err != nil { - log.Printf("unexpected error: %v\n", err) - continue - } - - fmt.Printf(" %s\n", a.Name) - for _, arg := range a.Arguments { - fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name]) - } - } - } + http.Handle("/metrics", promhttp.Handler()) + return http.ListenAndServe(listenAddress, nil) } -func GetEnv(name string, def string) string { +func getEnv(name string, def string) string { env := os.Getenv(name) if env != "" { return env @@ -114,7 +119,7 @@ func GetEnv(name string, def string) string { } } -func GetEnvInt(name string, def int) int { +func getEnvInt(name string, def int) int { env := os.Getenv(name) if env != "" { val, err := strconv.Atoi(env) diff --git a/metrics.go b/metrics.go index 23c6c52..2b40916 100644 --- a/metrics.go +++ b/metrics.go @@ -1,165 +1,135 @@ package main -import "github.com/prometheus/client_golang/prometheus" +import ( + _ "embed" + "fmt" + upnp "github.com/ndecker/fritzbox_exporter/fritzbox_upnp" + "github.com/prometheus/client_golang/prometheus" + "gopkg.in/yaml.v3" + "io" + "log" + "os" + "strings" +) + +//go:embed default-metrics.yaml +var defaultMetricsYaml []byte type Metric struct { - Service string - Action string - Result string - OkValue string + Metric string + Help string + Type string + + Service string + Action string + Result string + OkValue string `yaml:",omitempty"` + LabelName string `yaml:",omitempty"` + + Source string `yaml:",omitempty"` + ExampleValue string `yaml:",omitempty"` + + metricType prometheus.ValueType + desc *prometheus.Desc +} + +func (m *Metric) String() string { + var res strings.Builder + if m.Metric != "" { + res.WriteString(fmt.Sprintf("%s: ", m.Metric)) + } + + res.WriteString(fmt.Sprintf("%s/%s/%s", m.Service, m.Action, m.Result)) + + return res.String() +} + +func loadMetrics(data []byte) ([]*Metric, error) { + var metrics []*Metric + + err := yaml.Unmarshal(data, &metrics) + if err != nil { + return nil, err + } - Desc *prometheus.Desc - MetricType prometheus.ValueType + // Filter valid metrics + var metrics2 []*Metric + for _, m := range metrics { + if m.Metric == "" { + log.Printf("skipping metric %s: no metric name\n", m) + continue + } + + switch m.Type { + case "counter": + m.metricType = prometheus.CounterValue + case "gauge": + m.metricType = prometheus.GaugeValue + default: + log.Printf("skipping metric %s: invalid metric type: %s", m, m.Type) + continue + } + + labels := []string{"gateway"} + if m.LabelName != "" { + labels = append(labels, m.LabelName) + } + + m.desc = prometheus.NewDesc(m.Metric, m.Help, labels, nil) + metrics2 = append(metrics2, m) + } + + return metrics2, nil } -var IGDMetrics = []*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, - }, +func writeMetrics(w io.Writer, metrics []*Metric) error { + data, err := yaml.Marshal(metrics) + if err != nil { + return err + } + + _, err = w.Write(data) + return err } -var TR64Metrics = []*Metric{ - { - 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, - }, +func testMetrics(p upnp.ConnectionParameters, desc string) error { + root, err := upnp.LoadServiceRoot(p, desc) + if err != nil { + return err + } + + var metrics []*Metric + + for _, s := range root.Services { + for _, a := range s.Actions { + if !a.IsGetOnly() { + continue + } + + res, err := a.Call() + if err != nil { + log.Printf("unexpected error: %v\n", err) + continue + } + + for _, arg := range a.Arguments { + value := res[arg.StateVariable.Name] + + m := &Metric{ + Metric: "", + Help: "", + Type: "", + Service: s.ServiceType, + Action: a.Name, + Result: arg.StateVariable.Name, + ExampleValue: fmt.Sprintf("%v", value), + OkValue: "", + Source: desc, + } + metrics = append(metrics, m) + } + } + } + + return writeMetrics(os.Stdout, metrics) }