fix(inputs.upsd): Add additional fields to upsd from NUT (#14447)

This commit is contained in:
DaRK AnGeL 2024-01-05 17:46:26 +02:00 committed by GitHub
parent cbaca43e36
commit a1eb9f55c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 762 additions and 284 deletions

View File

@ -33,6 +33,19 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## consistently as floats to avoid database conflicts where some numbers are ## consistently as floats to avoid database conflicts where some numbers are
## parsed as integers and others as floats. ## parsed as integers and others as floats.
# force_float = false # force_float = false
## Collect additional fields if they are available for the UPS
## The fields need to be specified as NUT variable names, see
## https://networkupstools.org/docs/developer-guide.chunked/apas02.html
## Wildcards are accepted.
# additional_fields = []
## Dump information for debugging
## Allows to print the raw variables (and corresponding types) as received
## from the NUT server ONCE for each UPS. The output is only available when
## running Telegraf in debug-mode.
## Please attach this information when reporting issues!
# dump_raw_variables = false
``` ```
## Metrics ## Metrics

View File

@ -12,3 +12,16 @@
## consistently as floats to avoid database conflicts where some numbers are ## consistently as floats to avoid database conflicts where some numbers are
## parsed as integers and others as floats. ## parsed as integers and others as floats.
# force_float = false # force_float = false
## Collect additional fields if they are available for the UPS
## The fields need to be specified as NUT variable names, see
## https://networkupstools.org/docs/developer-guide.chunked/apas02.html
## Wildcards are accepted.
# additional_fields = []
## Dump information for debugging
## Allows to print the raw variables (and corresponding types) as received
## from the NUT server ONCE for each UPS. The output is only available when
## running Telegraf in debug-mode.
## Please attach this information when reporting issues!
# dump_raw_variables = false

View File

@ -0,0 +1 @@
upsd,model=CP900EPFCLCD,serial=0,status_OL=true,ups_name=fake battery_charge_percent=100i,battery_mfr_date="CPS",battery_runtime_low=300i,battery_voltage=24,firmware="",input_transfer_high=260i,input_transfer_low=170i,input_voltage=228,load_percent=13i,nominal_battery_voltage=24i,nominal_input_voltage=230i,nominal_power=540i,output_voltage=228,status_flags=8u,time_left_ns=4020000000000i,ups_delay_shutdown=20i,ups_delay_start=30i,ups_status="OL"

View File

@ -0,0 +1 @@
[[inputs.upsd]]

View File

@ -0,0 +1,49 @@
battery.charge: NUMBER
battery.charge.low: STRING
battery.charge.warning: NUMBER
battery.mfr.date: NUMBER
battery.runtime: NUMBER
battery.runtime.low: STRING
battery.type: NUMBER
battery.voltage: NUMBER
battery.voltage.nominal: NUMBER
device.mfr: NUMBER
device.model: NUMBER
device.serial: NUMBER
device.type: NUMBER
driver.debug: NUMBER
driver.flag.allow_killpower: NUMBER
driver.name: NUMBER
driver.parameter.pollfreq: NUMBER
driver.parameter.pollinterval: NUMBER
driver.parameter.port: NUMBER
driver.parameter.product: NUMBER
driver.parameter.productid: NUMBER
driver.parameter.serial: NUMBER
driver.parameter.synchronous: NUMBER
driver.parameter.vendor: NUMBER
driver.parameter.vendorid: NUMBER
driver.state: NUMBER
driver.version:
driver.version.data: NUMBER
driver.version.internal: NUMBER
driver.version.usb: NUMBER
input.transfer.high: STRING
input.transfer.low: STRING
input.voltage: NUMBER
input.voltage.nominal: NUMBER
output.voltage: NUMBER
ups.beeper.status: NUMBER
ups.delay.shutdown: STRING
ups.delay.start: STRING
ups.load: NUMBER
ups.mfr: NUMBER
ups.model: NUMBER
ups.productid: NUMBER
ups.realpower.nominal: NUMBER
ups.serial: NUMBER
ups.status: NUMBER
ups.test.result: NUMBER
ups.timer.shutdown: NUMBER
ups.timer.start: NUMBER
ups.vendorid: NUMBER

View File

@ -0,0 +1,49 @@
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.mfr.date: CPS
battery.runtime: 4020
battery.runtime.low: 300
battery.type: PbAcid
battery.voltage: 24.0
battery.voltage.nominal: 24
device.mfr: CPS
device.model: CP900EPFCLCD
device.serial: 000000000000
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: usbhid-ups
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.product: CP900EPFCLCD
driver.parameter.productid: 0501
driver.parameter.serial: 000000000000
driver.parameter.synchronous: auto
driver.parameter.vendor: CPS
driver.parameter.vendorid: 0764
driver.state: quiet
driver.version: 2.8.1
driver.version.data: CyberPower HID 0.8
driver.version.internal: 0.52
driver.version.usb: libusb-1.0.26 (API: 0x1000109)
input.transfer.high: 260
input.transfer.low: 170
input.voltage: 228.0
input.voltage.nominal: 230
output.voltage: 228.0
ups.beeper.status: enabled
ups.delay.shutdown: 20
ups.delay.start: 30
ups.load: 13
ups.mfr: CPS
ups.model: CP900EPFCLCD
ups.productid: 0501
ups.realpower.nominal: 540
ups.serial: 000000000000
ups.status: OL
ups.test.result: No test initiated
ups.timer.shutdown: -60
ups.timer.start: -60
ups.vendorid: 0764

View File

@ -0,0 +1 @@
upsd,model=CP900EPFCLCD,serial=0,status_OL=true,ups_name=fake battery_charge_low=10i,battery_charge_percent=100i,battery_charge_warning=20i,battery_mfr_date="CPS",battery_runtime_low=300i,battery_type="PbAcid",battery_voltage=24,firmware="",input_transfer_high=260i,input_transfer_low=170i,input_voltage=228,load_percent=13i,nominal_battery_voltage=24i,nominal_input_voltage=230i,nominal_power=540i,output_voltage=228,status_flags=8u,time_left_ns=4020000000000i,ups_delay_shutdown=20i,ups_delay_start=30i,ups_status="OL" 1704213086754003102

View File

@ -0,0 +1,2 @@
[[inputs.upsd]]
additional_fields = ["battery.*"]

View File

@ -0,0 +1,49 @@
battery.charge: NUMBER
battery.charge.low: STRING
battery.charge.warning: NUMBER
battery.mfr.date: NUMBER
battery.runtime: NUMBER
battery.runtime.low: STRING
battery.type: NUMBER
battery.voltage: NUMBER
battery.voltage.nominal: NUMBER
device.mfr: NUMBER
device.model: NUMBER
device.serial: NUMBER
device.type: NUMBER
driver.debug: NUMBER
driver.flag.allow_killpower: NUMBER
driver.name: NUMBER
driver.parameter.pollfreq: NUMBER
driver.parameter.pollinterval: NUMBER
driver.parameter.port: NUMBER
driver.parameter.product: NUMBER
driver.parameter.productid: NUMBER
driver.parameter.serial: NUMBER
driver.parameter.synchronous: NUMBER
driver.parameter.vendor: NUMBER
driver.parameter.vendorid: NUMBER
driver.state: NUMBER
driver.version:
driver.version.data: NUMBER
driver.version.internal: NUMBER
driver.version.usb: NUMBER
input.transfer.high: STRING
input.transfer.low: STRING
input.voltage: NUMBER
input.voltage.nominal: NUMBER
output.voltage: NUMBER
ups.beeper.status: NUMBER
ups.delay.shutdown: STRING
ups.delay.start: STRING
ups.load: NUMBER
ups.mfr: NUMBER
ups.model: NUMBER
ups.productid: NUMBER
ups.realpower.nominal: NUMBER
ups.serial: NUMBER
ups.status: NUMBER
ups.test.result: NUMBER
ups.timer.shutdown: NUMBER
ups.timer.start: NUMBER
ups.vendorid: NUMBER

View File

@ -0,0 +1,49 @@
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.mfr.date: CPS
battery.runtime: 4020
battery.runtime.low: 300
battery.type: PbAcid
battery.voltage: 24.0
battery.voltage.nominal: 24
device.mfr: CPS
device.model: CP900EPFCLCD
device.serial: 000000000000
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: usbhid-ups
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.product: CP900EPFCLCD
driver.parameter.productid: 0501
driver.parameter.serial: 000000000000
driver.parameter.synchronous: auto
driver.parameter.vendor: CPS
driver.parameter.vendorid: 0764
driver.state: quiet
driver.version: 2.8.1
driver.version.data: CyberPower HID 0.8
driver.version.internal: 0.52
driver.version.usb: libusb-1.0.26 (API: 0x1000109)
input.transfer.high: 260
input.transfer.low: 170
input.voltage: 228.0
input.voltage.nominal: 230
output.voltage: 228.0
ups.beeper.status: enabled
ups.delay.shutdown: 20
ups.delay.start: 30
ups.load: 13
ups.mfr: CPS
ups.model: CP900EPFCLCD
ups.productid: 0501
ups.realpower.nominal: 540
ups.serial: 000000000000
ups.status: OL
ups.test.result: No test initiated
ups.timer.shutdown: -60
ups.timer.start: -60
ups.vendorid: 0764

View File

@ -0,0 +1 @@
upsd,model=CP900EPFCLCD,serial=0,status_OL=true,ups_name=fake battery_charge_low=10i,battery_charge_percent=100i,battery_charge_warning=20i,battery_mfr_date="CPS",battery_runtime_low=300i,battery_type="PbAcid",battery_voltage=24,device_mfr="CPS",device_type="ups",driver_debug=0i,driver_flag_allow_killpower=0i,driver_name="usbhid-ups",driver_parameter_pollfreq=30i,driver_parameter_pollinterval=2i,driver_parameter_port="auto",driver_parameter_product="CP900EPFCLCD",driver_parameter_productid=501i,driver_parameter_serial=0i,driver_parameter_synchronous="auto",driver_parameter_vendor="CPS",driver_parameter_vendorid=764i,driver_state="quiet",driver_version="2.8.1",driver_version_data="CyberPower HID 0.8",driver_version_internal=0.52,driver_version_usb="libusb-1.0.26 (API: 0x1000109)",firmware="",input_transfer_high=260i,input_transfer_low=170i,input_voltage=228,load_percent=13i,nominal_battery_voltage=24i,nominal_input_voltage=230i,nominal_power=540i,output_voltage=228,status_flags=8u,time_left_ns=4020000000000i,ups_beeper_status=true,ups_delay_shutdown=20i,ups_delay_start=30i,ups_mfr="CPS",ups_model="CP900EPFCLCD",ups_productid=501i,ups_serial=0i,ups_status="OL",ups_test_result="No test initiated",ups_timer_shutdown=-60i,ups_timer_start=-60i,ups_vendorid=764i

View File

@ -0,0 +1,2 @@
[[inputs.upsd]]
additional_fields = ["*"]

View File

@ -0,0 +1,49 @@
battery.charge: NUMBER
battery.charge.low: STRING
battery.charge.warning: NUMBER
battery.mfr.date: NUMBER
battery.runtime: NUMBER
battery.runtime.low: STRING
battery.type: NUMBER
battery.voltage: NUMBER
battery.voltage.nominal: NUMBER
device.mfr: NUMBER
device.model: NUMBER
device.serial: NUMBER
device.type: NUMBER
driver.debug: NUMBER
driver.flag.allow_killpower: NUMBER
driver.name: NUMBER
driver.parameter.pollfreq: NUMBER
driver.parameter.pollinterval: NUMBER
driver.parameter.port: NUMBER
driver.parameter.product: NUMBER
driver.parameter.productid: NUMBER
driver.parameter.serial: NUMBER
driver.parameter.synchronous: NUMBER
driver.parameter.vendor: NUMBER
driver.parameter.vendorid: NUMBER
driver.state: NUMBER
driver.version:
driver.version.data: NUMBER
driver.version.internal: NUMBER
driver.version.usb: NUMBER
input.transfer.high: STRING
input.transfer.low: STRING
input.voltage: NUMBER
input.voltage.nominal: NUMBER
output.voltage: NUMBER
ups.beeper.status: NUMBER
ups.delay.shutdown: STRING
ups.delay.start: STRING
ups.load: NUMBER
ups.mfr: NUMBER
ups.model: NUMBER
ups.productid: NUMBER
ups.realpower.nominal: NUMBER
ups.serial: NUMBER
ups.status: NUMBER
ups.test.result: NUMBER
ups.timer.shutdown: NUMBER
ups.timer.start: NUMBER
ups.vendorid: NUMBER

View File

@ -0,0 +1,49 @@
battery.charge: 100
battery.charge.low: 10
battery.charge.warning: 20
battery.mfr.date: CPS
battery.runtime: 4020
battery.runtime.low: 300
battery.type: PbAcid
battery.voltage: 24.0
battery.voltage.nominal: 24
device.mfr: CPS
device.model: CP900EPFCLCD
device.serial: 000000000000
device.type: ups
driver.debug: 0
driver.flag.allow_killpower: 0
driver.name: usbhid-ups
driver.parameter.pollfreq: 30
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.parameter.product: CP900EPFCLCD
driver.parameter.productid: 0501
driver.parameter.serial: 000000000000
driver.parameter.synchronous: auto
driver.parameter.vendor: CPS
driver.parameter.vendorid: 0764
driver.state: quiet
driver.version: 2.8.1
driver.version.data: CyberPower HID 0.8
driver.version.internal: 0.52
driver.version.usb: libusb-1.0.26 (API: 0x1000109)
input.transfer.high: 260
input.transfer.low: 170
input.voltage: 228.0
input.voltage.nominal: 230
output.voltage: 228.0
ups.beeper.status: enabled
ups.delay.shutdown: 20
ups.delay.start: 30
ups.load: 13
ups.mfr: CPS
ups.model: CP900EPFCLCD
ups.productid: 0501
ups.realpower.nominal: 540
ups.serial: 000000000000
ups.status: OL
ups.test.result: No test initiated
ups.timer.shutdown: -60
ups.timer.start: -60
ups.vendorid: 0764

View File

@ -0,0 +1 @@
upsd,model=Model\ 12345,serial=ABC123,status_OL=true,ups_name=fake battery_charge_percent=100,battery_mfr_date="2016-07-26",battery_voltage=13.4,firmware="CUSTOM_FIRMWARE",input_voltage=242,load_percent=23,nominal_battery_voltage=24,nominal_input_voltage=230,nominal_power=700i,output_voltage=230,real_power=41,status_flags=8u,time_left_ns=600000000000i,ups_status="OL"

View File

@ -0,0 +1 @@
[[inputs.upsd]]

View File

@ -0,0 +1,15 @@
device.serial: STRING:64
device.model: STRING:64
input.voltage: NUMBER
ups.load: NUMBER
battery.charge: NUMBER
battery.runtime: NUMBER
output.voltage: NUMBER
battery.voltage: NUMBER
input.voltage.nominal: NUMBER
battery.voltage.nominal: NUMBER
ups.realpower: NUMBER
ups.realpower.nominal: NUMBER
ups.firmware: STRING:64
battery.mfr.date: STRING:64
ups.status: STRING:64

View File

@ -0,0 +1,15 @@
device.serial: ABC123
device.model: Model 12345
input.voltage: 242.0
ups.load: 23.0
battery.charge: 100.0
battery.runtime: 600.00
output.voltage: 230.0
battery.voltage: 13.4
input.voltage.nominal: 230.0
battery.voltage.nominal: 24.0
ups.realpower: 41.0
ups.realpower.nominal: 700
ups.firmware: CUSTOM_FIRMWARE
battery.mfr.date: 2016-07-26
ups.status: OL

View File

@ -0,0 +1 @@
upsd,model=Model\ 12345,serial=ABC123,status_OL=true,ups_name=fake battery_charge_percent=100,battery_mfr_date="2016-07-26",battery_voltage=13.4,firmware="CUSTOM_FIRMWARE",input_voltage=242,load_percent=23,nominal_battery_voltage=24,nominal_input_voltage=230,nominal_power=700,output_voltage=230,real_power=41,status_flags=8u,time_left_ns=600000000000i,ups_status="OL"

View File

@ -0,0 +1,2 @@
[[inputs.upsd]]
force_float = true

View File

@ -0,0 +1,15 @@
device.serial: STRING:64
device.model: STRING:64
input.voltage: NUMBER
ups.load: NUMBER
battery.charge: NUMBER
battery.runtime: NUMBER
output.voltage: NUMBER
battery.voltage: NUMBER
input.voltage.nominal: NUMBER
battery.voltage.nominal: NUMBER
ups.realpower: NUMBER
ups.realpower.nominal: NUMBER
ups.firmware: STRING:64
battery.mfr.date: STRING:64
ups.status: STRING:64

View File

@ -0,0 +1,15 @@
device.serial: ABC123
device.model: Model 12345
input.voltage: 242.0
ups.load: 23.0
battery.charge: 100.0
battery.runtime: 600.00
output.voltage: 230.0
battery.voltage: 13.4
input.voltage.nominal: 230.0
battery.voltage.nominal: 24.0
ups.realpower: 41.0
ups.realpower.nominal: 700
ups.firmware: CUSTOM_FIRMWARE
battery.mfr.date: 2016-07-26
ups.status: OL

View File

@ -0,0 +1 @@
upsd,model=Model\ 12345,serial=ABC123,status_OL=true,ups_name=fake battery_charge_percent=100,battery_mfr_date="2016-07-26",battery_voltage=13.4,firmware="CUSTOM_FIRMWARE",input_voltage=242,load_percent=23,nominal_battery_voltage=24,nominal_input_voltage=230,nominal_power=700,output_voltage=230,real_power=41,status_flags=8u,time_left_ns=600000000000i,ups_status="OL",device_location="Upper floor"

View File

@ -0,0 +1,3 @@
[[inputs.upsd]]
force_float = true
additional_fields = ["*"]

View File

@ -0,0 +1,16 @@
device.serial: STRING:64
device.model: STRING:64
device.location: STRING:64
input.voltage: NUMBER
ups.load: NUMBER
battery.charge: NUMBER
battery.runtime: NUMBER
output.voltage: NUMBER
battery.voltage: NUMBER
input.voltage.nominal: NUMBER
battery.voltage.nominal: NUMBER
ups.realpower: NUMBER
ups.realpower.nominal: NUMBER
ups.firmware: STRING:64
battery.mfr.date: STRING:64
ups.status: STRING:64

View File

@ -0,0 +1,16 @@
device.serial: ABC123
device.model: Model 12345
device.location: Upper floor
input.voltage: 242.0
ups.load: 23.0
battery.charge: 100.0
battery.runtime: 600.00
output.voltage: 230.0
battery.voltage: 13.4
input.voltage.nominal: 230.0
battery.voltage.nominal: 24.0
ups.realpower: 41.0
ups.realpower.nominal: 700
ups.firmware: CUSTOM_FIRMWARE
battery.mfr.date: 2016-07-26
ups.status: OL

View File

@ -9,6 +9,7 @@ import (
nut "github.com/robbiet480/go.nut" nut "github.com/robbiet480/go.nut"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/filter"
"github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/internal/choice" "github.com/influxdata/telegraf/internal/choice"
"github.com/influxdata/telegraf/plugins/inputs" "github.com/influxdata/telegraf/plugins/inputs"
@ -17,37 +18,99 @@ import (
//go:embed sample.conf //go:embed sample.conf
var sampleConfig string var sampleConfig string
//See: https://networkupstools.org/docs/developer-guide.chunked/index.html // see: https://networkupstools.org/docs/developer-guide.chunked/index.html
const defaultAddress = "127.0.0.1" const defaultAddress = "127.0.0.1"
const defaultPort = 3493 const defaultPort = 3493
type Upsd struct { // Define the set of variables _always_ included in a metric
Server string `toml:"server"` var mandatoryVariableSet = map[string]bool{
Port int `toml:"port"` "battery.date": true,
Username string `toml:"username"` "battery.mfr.date": true,
Password string `toml:"password"` "battery.runtime": true,
ForceFloat bool `toml:"force_float"` "device.model": true,
"device.serial": true,
"ups.firmware": true,
"ups.status": true,
}
Log telegraf.Logger `toml:"-"` // Define the default field set to add if existing
var defaultFieldSet = map[string]string{
"battery.charge": "battery_charge_percent",
"battery.runtime.low": "battery_runtime_low",
"battery.voltage": "battery_voltage",
"input.frequency": "input_frequency",
"input.transfer.high": "input_transfer_high",
"input.transfer.low": "input_transfer_low",
"input.voltage": "input_voltage",
"ups.temperature": "internal_temp",
"ups.load": "load_percent",
"battery.voltage.nominal": "nominal_battery_voltage",
"input.voltage.nominal": "nominal_input_voltage",
"ups.realpower.nominal": "nominal_power",
"output.voltage": "output_voltage",
"ups.realpower": "real_power",
"ups.delay.shutdown": "ups_delay_shutdown",
"ups.delay.start": "ups_delay_start",
}
type Upsd struct {
Server string `toml:"server"`
Port int `toml:"port"`
Username string `toml:"username"`
Password string `toml:"password"`
ForceFloat bool `toml:"force_float"`
Additional []string `toml:"additional_fields"`
DumpRaw bool `toml:"dump_raw_variables"`
Log telegraf.Logger `toml:"-"`
filter filter.Filter
dumped map[string]bool
} }
func (*Upsd) SampleConfig() string { func (*Upsd) SampleConfig() string {
return sampleConfig return sampleConfig
} }
func (u *Upsd) Init() error {
// Compile the additional fields filter
f, err := filter.Compile(u.Additional)
if err != nil {
return fmt.Errorf("compiling additional_fields filter failed: %w", err)
}
u.filter = f
u.dumped = make(map[string]bool)
return nil
}
func (u *Upsd) Gather(acc telegraf.Accumulator) error { func (u *Upsd) Gather(acc telegraf.Accumulator) error {
upsList, err := u.fetchVariables(u.Server, u.Port) upsList, err := u.fetchVariables(u.Server, u.Port)
if err != nil { if err != nil {
return err return err
} }
if u.DumpRaw {
for name, variables := range upsList {
// Only dump the information once per UPS
if u.dumped[name] {
continue
}
values := make([]string, 0, len(variables))
types := make([]string, 0, len(variables))
for _, v := range variables {
values = append(values, fmt.Sprintf("%s: %v", v.Name, v.Value))
types = append(types, fmt.Sprintf("%s: %v", v.Name, v.OriginalType))
}
u.Log.Debugf("Variables dump for UPS %q:\n%s\n-----\n%s", name, strings.Join(values, "\n"), strings.Join(types, "\n"))
}
}
for name, variables := range upsList { for name, variables := range upsList {
u.gatherUps(acc, name, variables) u.gatherUps(acc, name, variables)
} }
return nil return nil
} }
func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.Variable) { func (u *Upsd) gatherUps(acc telegraf.Accumulator, upsname string, variables []nut.Variable) {
metrics := make(map[string]interface{}) metrics := make(map[string]interface{})
for _, variable := range variables { for _, variable := range variables {
name := variable.Name name := variable.Name
@ -57,7 +120,7 @@ func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.
tags := map[string]string{ tags := map[string]string{
"serial": fmt.Sprintf("%v", metrics["device.serial"]), "serial": fmt.Sprintf("%v", metrics["device.serial"]),
"ups_name": name, "ups_name": upsname,
//"variables": variables.Status not sure if it's a good idea to provide this //"variables": variables.Status not sure if it's a good idea to provide this
"model": fmt.Sprintf("%v", metrics["device.model"]), "model": fmt.Sprintf("%v", metrics["device.model"]),
} }
@ -75,54 +138,18 @@ func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.
u.Log.Warnf("Converting 'battery.runtime' to 'time_left_ns' failed: %v", err) u.Log.Warnf("Converting 'battery.runtime' to 'time_left_ns' failed: %v", err)
} }
// Add the mandatory information
fields := map[string]interface{}{ fields := map[string]interface{}{
"battery_date": metrics["battery.date"], "battery_date": metrics["battery.date"],
"battery_mfr_date": metrics["battery.mfr.date"], "battery_mfr_date": metrics["battery.mfr.date"],
"status_flags": status, "status_flags": status,
"ups_status": metrics["ups.status"], "ups_status": metrics["ups.status"],
//Compatibility with apcupsd metrics format // for compatibility with apcupsd metrics format
"time_left_ns": timeLeftNS, "time_left_ns": timeLeftNS,
} }
floatValues := map[string]string{ // Define the set of mandatory string fields
"battery_charge_percent": "battery.charge",
"battery_runtime_low": "battery.runtime.low",
"battery_voltage": "battery.voltage",
"input_frequency": "input.frequency",
"input_transfer_high": "input.transfer.high",
"input_transfer_low": "input.transfer.low",
"input_voltage": "input.voltage",
"internal_temp": "ups.temperature",
"load_percent": "ups.load",
"nominal_battery_voltage": "battery.voltage.nominal",
"nominal_input_voltage": "input.voltage.nominal",
"nominal_power": "ups.realpower.nominal",
"output_voltage": "output.voltage",
"real_power": "ups.realpower",
"ups_delay_shutdown": "ups.delay.shutdown",
"ups_delay_start": "ups.delay.start",
}
for key, rawValue := range floatValues {
if metrics[rawValue] == nil {
continue
}
if !u.ForceFloat {
fields[key] = metrics[rawValue]
continue
}
// Force expected float values to actually being float (e.g. if delivered as int)
float, err := internal.ToFloat64(metrics[rawValue])
if err != nil {
acc.AddError(fmt.Errorf("converting %s=%v failed: %w", rawValue, metrics[rawValue], err))
continue
}
fields[key] = float
}
val, err := internal.ToString(metrics["ups.firmware"]) val, err := internal.ToString(metrics["ups.firmware"])
if err != nil { if err != nil {
acc.AddError(fmt.Errorf("converting ups.firmware=%q failed: %w", metrics["ups.firmware"], err)) acc.AddError(fmt.Errorf("converting ups.firmware=%q failed: %w", metrics["ups.firmware"], err))
@ -130,6 +157,36 @@ func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.
fields["firmware"] = val fields["firmware"] = val
} }
// Try to gather all default fields and optional field
for varname, v := range metrics {
// Skip all empty fields and all fields contained in the mandatory set
// of fields added above.
if v == nil || mandatoryVariableSet[varname] {
continue
}
// Use the name of the default field-set if present and otherwise check
// the additional field-set. If none of them contains the variable, we
// skip over it
var key string
if k, found := defaultFieldSet[varname]; found {
key = k
} else if u.filter != nil && u.filter.Match(varname) {
key = strings.ReplaceAll(varname, ".", "_")
} else {
continue
}
// Force expected float values to actually being float (e.g. if delivered as int)
if u.ForceFloat {
float, err := internal.ToFloat64(v)
if err == nil {
v = float
}
}
fields[key] = v
}
acc.AddFields("upsd", fields, tags) acc.AddFields("upsd", fields, tags)
} }

View File

@ -1,159 +1,192 @@
package upsd package upsd
import ( import (
"bufio"
"bytes"
"context" "context"
"fmt"
"net" "net"
"os"
"path/filepath"
"strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/plugins/parsers/influx"
"github.com/influxdata/telegraf/testutil" "github.com/influxdata/telegraf/testutil"
) )
func TestUpsdGather(t *testing.T) { func TestBadServer(t *testing.T) {
nut := &Upsd{} // Create and start a server without interactions
server := &mockServer{}
ctx, cancel := context.WithCancel(context.Background())
addr, err := server.listen(ctx)
require.NoError(t, err)
defer cancel()
var ( // Setup the plugin
tests = []struct { plugin := &Upsd{
name string Server: addr.IP.String(),
forceFloat bool Port: addr.Port,
err bool }
tags map[string]string require.NoError(t, plugin.Init())
fields map[string]interface{}
out func() []interaction // Do the query
}{ var acc testutil.Accumulator
{ require.Error(t, plugin.Gather(&acc))
name: "test listening server with output", }
forceFloat: false,
err: false, func TestCases(t *testing.T) {
tags: map[string]string{ // Get all directories in testdata
"serial": "ABC123", folders, err := os.ReadDir("testcases")
"ups_name": "fake", require.NoError(t, err)
"model": "Model 12345",
"status_OL": "true", // Register the plugin
}, inputs.Add("upsd", func() telegraf.Input {
fields: map[string]interface{}{ return &Upsd{}
"battery_charge_percent": float64(100), })
"battery_date": nil,
"battery_mfr_date": "2016-07-26", for _, f := range folders {
"battery_voltage": float64(13.4), // Only handle folders
"firmware": "CUSTOM_FIRMWARE", if !f.IsDir() {
"input_voltage": float64(242), continue
"load_percent": float64(23),
"nominal_battery_voltage": float64(24),
"nominal_input_voltage": float64(230),
"nominal_power": int64(700),
"output_voltage": float64(230),
"real_power": float64(41),
"status_flags": uint64(8),
"time_left_ns": int64(600000000000),
"ups_status": "OL",
},
out: genOutput,
},
{
name: "test listening server with output & force floats",
forceFloat: true,
err: false,
tags: map[string]string{
"serial": "ABC123",
"ups_name": "fake",
"model": "Model 12345",
"status_OL": "true",
},
fields: map[string]interface{}{
"battery_charge_percent": float64(100),
"battery_date": nil,
"battery_mfr_date": "2016-07-26",
"battery_voltage": float64(13.4),
"firmware": "CUSTOM_FIRMWARE",
"input_voltage": float64(242),
"load_percent": float64(23),
"nominal_battery_voltage": float64(24),
"nominal_input_voltage": float64(230),
"nominal_power": int64(700),
"output_voltage": float64(230),
"real_power": float64(41),
"status_flags": uint64(8),
"time_left_ns": int64(600000000000),
"ups_status": "OL",
},
out: genOutput,
},
} }
testcasePath := filepath.Join("testcases", f.Name())
configFilename := filepath.Join(testcasePath, "telegraf.conf")
expectedFilename := filepath.Join(testcasePath, "expected.out")
acc testutil.Accumulator t.Run(f.Name(), func(t *testing.T) {
) // Prepare the influx parser for expectations
parser := &influx.Parser{}
require.NoError(t, parser.Init())
for _, tt := range tests { // Read the expected output if any
t.Run(tt.name, func(t *testing.T) { var expected []telegraf.Metric
ctx, cancel := context.WithCancel(context.Background()) if _, err := os.Stat(expectedFilename); err == nil {
var err error
expected, err = testutil.ParseMetricsFromFile(expectedFilename, parser)
require.NoError(t, err)
}
lAddr, err := listen(ctx, t, tt.out()) // Setup a server from the input data
server, err := setupServer(testcasePath)
require.NoError(t, err) require.NoError(t, err)
nut.Server = (lAddr.(*net.TCPAddr)).IP.String() // Start the server
nut.Port = (lAddr.(*net.TCPAddr)).Port ctx, cancel := context.WithCancel(context.Background())
nut.ForceFloat = tt.forceFloat addr, err := server.listen(ctx)
require.NoError(t, err)
defer cancel()
err = nut.Gather(&acc) // Configure the plugin
if tt.err { cfg := config.NewConfig()
require.Error(t, err) require.NoError(t, cfg.LoadConfig(configFilename))
} else { require.Len(t, cfg.Inputs, 1)
require.NoError(t, err) plugin := cfg.Inputs[0].Input.(*Upsd)
acc.AssertContainsFields(t, "upsd", tt.fields) plugin.Server = addr.IP.String()
acc.AssertContainsTaggedFields(t, "upsd", tt.fields, tt.tags) plugin.Port = addr.Port
} require.NoError(t, plugin.Init())
cancel()
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
// Check the metric nevertheless as we might get some metrics despite errors.
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
acc.Lock()
defer acc.Unlock()
require.Empty(t, acc.Errors)
}) })
} }
} }
func TestUpsdGatherFail(t *testing.T) { type interaction struct {
nut := &Upsd{} Expected string
Response string
}
var ( type variable struct {
tests = []struct { Name string
name string Value string
err bool }
tags map[string]string
fields map[string]interface{}
out func() []interaction
}{
{
name: "test with bad output",
err: true,
out: genBadOutput,
},
}
acc testutil.Accumulator type mockServer struct {
) protocol []interaction
}
for _, tt := range tests { func (s *mockServer) init() {
t.Run(tt.name, func(t *testing.T) { s.protocol = []interaction{
ctx, cancel := context.WithCancel(context.Background()) {
Expected: "VER\n",
lAddr, err := listen(ctx, t, tt.out()) Response: "1\n",
require.NoError(t, err) },
{
nut.Server = (lAddr.(*net.TCPAddr)).IP.String() Expected: "NETVER\n",
nut.Port = (lAddr.(*net.TCPAddr)).Port Response: "1\n",
},
err = nut.Gather(&acc) {
if tt.err { Expected: "LIST UPS\n",
require.Error(t, err) Response: "BEGIN LIST UPS\nUPS fake \"fake UPS\"\nEND LIST UPS\n",
} else { },
require.NoError(t, err) {
acc.AssertContainsTaggedFields(t, "upsd", tt.fields, tt.tags) Expected: "LIST CLIENT fake\n",
} Response: "BEGIN LIST CLIENT fake\nCLIENT fake 127.0.0.1\nEND LIST CLIENT fake\n",
cancel() },
}) {
Expected: "LIST CMD fake\n",
Response: "BEGIN LIST CMD fake\nEND LIST CMD fake\n",
},
{
Expected: "GET UPSDESC fake\n",
Response: "UPSDESC fake \"stub-ups-description\"\n",
},
{
Expected: "GET NUMLOGINS fake\n",
Response: "NUMLOGINS fake 1\n",
},
} }
} }
func listen(ctx context.Context, t *testing.T, out []interaction) (net.Addr, error) { func (s *mockServer) addVariables(variables []variable, types map[string]string) error {
// Add a VAR entries for the variables
values := make([]string, 0, len(variables))
for _, v := range variables {
values = append(values, fmt.Sprintf("VAR fake %s %q", v.Name, v.Value))
}
s.protocol = append(s.protocol, interaction{
Expected: "LIST VAR fake\n",
Response: "BEGIN LIST VAR fake\n" + strings.Join(values, "\n") + "\nEND LIST VAR fake\n",
})
// Add a description and type interaction for the variable
for _, v := range variables {
variableType, found := types[v.Name]
if !found {
return fmt.Errorf("type for variable %q not found", v.Name)
}
s.protocol = append(s.protocol,
interaction{
Expected: "GET DESC fake " + v.Name + "\n",
Response: "DESC fake" + v.Name + " \"No description here\"\n",
},
interaction{
Expected: "GET TYPE fake " + v.Name + "\n",
Response: "TYPE fake " + v.Name + " " + variableType + "\n",
},
)
}
return nil
}
func (s *mockServer) listen(ctx context.Context) (*net.TCPAddr, error) {
lc := net.ListenConfig{} lc := net.ListenConfig{}
ln, err := lc.Listen(ctx, "tcp4", "127.0.0.1:0") ln, err := lc.Listen(ctx, "tcp4", "127.0.0.1:0")
if err != nil { if err != nil {
@ -170,131 +203,90 @@ func listen(ctx context.Context, t *testing.T, out []interaction) (net.Addr, err
return return
} }
defer conn.Close() defer conn.Close()
require.NoError(t, conn.SetReadDeadline(time.Now().Add(time.Minute))) _ = conn.SetReadDeadline(time.Now().Add(time.Minute))
in := make([]byte, 128) in := make([]byte, 128)
for _, interaction := range out { for _, interaction := range s.protocol {
n, err := conn.Read(in) n, err := conn.Read(in)
require.NoError(t, err, "failed to read from connection") if err != nil {
fmt.Printf("Failed to read from connection: %v\n", err)
return
}
expectedBytes := []byte(interaction.Expected) request := in[:n]
want, got := expectedBytes, in[:n] if !bytes.Equal([]byte(interaction.Expected), request) {
require.Equal(t, want, got) fmt.Printf("Unexpected request %q, expected %q\n", string(request), interaction.Expected)
return
}
_, err = conn.Write([]byte(interaction.Response)) if _, err := conn.Write([]byte(interaction.Response)); err != nil {
require.NoError(t, err, "failed to respond to LIST UPS") fmt.Printf("Cannot write answer for request %q: %v\n", string(request), err)
return
}
} }
// Append EOF to end of output bytes // Append EOF to end of output bytes
_, err = conn.Write([]byte{0, 0}) if _, err := conn.Write([]byte{0, 0}); err != nil {
require.NoError(t, err, "failed to write EOF") fmt.Printf("Cannot write EOF: %v\n", err)
return
}
}() }()
} }
}() }()
return ln.Addr(), nil return ln.Addr().(*net.TCPAddr), nil
} }
type interaction struct { func setupServer(path string) (*mockServer, error) {
Expected string // Read the variables
Response string varbuf, err := os.ReadFile(filepath.Join(path, "variables.dev"))
} if err != nil {
return nil, fmt.Errorf("reading variables failed: %w", err)
}
func genOutput() []interaction { // Parse the information into variable names and values (upsc format)
m := make([]interaction, 0) variables := make([]variable, 0)
m = append(m, scanner := bufio.NewScanner(bytes.NewBuffer(varbuf))
interaction{ for scanner.Scan() {
Expected: "VER\n", line := scanner.Text()
Response: "1\n", parts := strings.SplitN(line, ":", 2)
}, if len(parts) != 2 {
interaction{ return nil, fmt.Errorf("cannot parse line %s", line)
Expected: "NETVER\n", }
Response: "1\n", name := strings.TrimSpace(parts[0])
}, value := strings.TrimSpace(parts[1])
interaction{ variables = append(variables, variable{name, value})
Expected: "LIST UPS\n", }
Response: `BEGIN LIST UPS if err := scanner.Err(); err != nil {
UPS fake "fakescription" return nil, fmt.Errorf("processing variables failed: %w", err)
END LIST UPS }
`,
},
interaction{
Expected: "LIST CLIENT fake\n",
Response: `BEGIN LIST CLIENT fake
CLIENT fake 192.168.1.1
END LIST CLIENT fake
`,
},
interaction{
Expected: "LIST CMD fake\n",
Response: `BEGIN LIST CMD fake
END LIST CMD fake
`,
},
interaction{
Expected: "GET UPSDESC fake\n",
Response: "UPSDESC fake \"stub-ups-description\"\n",
},
interaction{
Expected: "GET NUMLOGINS fake\n",
Response: "NUMLOGINS fake 1\n",
},
interaction{
Expected: "LIST VAR fake\n",
Response: `BEGIN LIST VAR fake
VAR fake device.serial "ABC123"
VAR fake device.model "Model 12345"
VAR fake input.voltage "242.0"
VAR fake ups.load "23.0"
VAR fake battery.charge "100.0"
VAR fake battery.runtime "600.00"
VAR fake output.voltage "230.0"
VAR fake battery.voltage "13.4"
VAR fake input.voltage.nominal "230.0"
VAR fake battery.voltage.nominal "24.0"
VAR fake ups.realpower "41.0"
VAR fake ups.realpower.nominal "700"
VAR fake ups.firmware "CUSTOM_FIRMWARE"
VAR fake battery.mfr.date "2016-07-26"
VAR fake ups.status "OL"
END LIST VAR fake
`,
},
)
m = appendVariable(m, "device.serial", "STRING:64")
m = appendVariable(m, "device.model", "STRING:64")
m = appendVariable(m, "input.voltage", "NUMBER")
m = appendVariable(m, "ups.load", "NUMBER")
m = appendVariable(m, "battery.charge", "NUMBER")
m = appendVariable(m, "battery.runtime", "NUMBER")
m = appendVariable(m, "output.voltage", "NUMBER")
m = appendVariable(m, "battery.voltage", "NUMBER")
m = appendVariable(m, "input.voltage.nominal", "NUMBER")
m = appendVariable(m, "battery.voltage.nominal", "NUMBER")
m = appendVariable(m, "ups.realpower", "NUMBER")
m = appendVariable(m, "ups.realpower.nominal", "NUMBER")
m = appendVariable(m, "ups.firmware", "STRING:64")
m = appendVariable(m, "battery.mfr.date", "STRING:64")
m = appendVariable(m, "ups.status", "STRING:64")
return m // Read the variable-type mapping
} typebuf, err := os.ReadFile(filepath.Join(path, "types.dev"))
if err != nil {
return nil, fmt.Errorf("reading variables failed: %w", err)
}
func appendVariable(m []interaction, name string, typ string) []interaction { // Parse the information into variable names and values (upsc format)
m = append(m, types := make(map[string]string, 0)
interaction{ scanner = bufio.NewScanner(bytes.NewBuffer(typebuf))
Expected: "GET DESC fake " + name + "\n", for scanner.Scan() {
Response: "DESC fake" + name + " \"No description here\"\n", line := scanner.Text()
}, parts := strings.SplitN(line, ":", 2)
interaction{ if len(parts) != 2 {
Expected: "GET TYPE fake " + name + "\n", return nil, fmt.Errorf("cannot parse line %s", line)
Response: "TYPE fake " + name + " " + typ + "\n", }
}, name := strings.TrimSpace(parts[0])
) vartype := strings.TrimSpace(parts[1])
return m types[name] = vartype
} }
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("processing variables failed: %w", err)
}
func genBadOutput() []interaction { // Setup the server and add the device information
m := make([]interaction, 0) server := &mockServer{}
return m server.init()
err = server.addVariables(variables, types)
return server, err
} }