Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(input): add upsd implementation #9890

Merged
merged 20 commits into from
Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ following works:
- github.com/rcrowley/go-metrics [MIT License](https://github.com/rcrowley/go-metrics/blob/master/LICENSE)
- github.com/remyoudompheng/bigfft [BSD 3-Clause "New" or "Revised" License](https://github.com/remyoudompheng/bigfft/blob/master/LICENSE)
- github.com/riemann/riemann-go-client [MIT License](https://github.com/riemann/riemann-go-client/blob/master/LICENSE)
- github.com/robbiet480/go.nut [MIT License](https://github.com/robbiet480/go.nut/blob/master/LICENSE)
- github.com/safchain/ethtool [Apache License 2.0](https://github.com/safchain/ethtool/blob/master/LICENSE)
- github.com/samuel/go-zookeeper [BSD 3-Clause Clear License](https://github.com/samuel/go-zookeeper/blob/master/LICENSE)
- github.com/shirou/gopsutil [BSD 3-Clause Clear License](https://github.com/shirou/gopsutil/blob/master/LICENSE)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ require (
github.com/prometheus/prometheus v1.8.2-0.20210430082741-2a4b8e12bbf2
github.com/rabbitmq/amqp091-go v1.3.4
github.com/riemann/riemann-go-client v0.5.1-0.20211206220514-f58f10cdce16
github.com/robbiet480/go.nut v0.0.0-20220219091450-bd8f121e1fa1
github.com/safchain/ethtool v0.0.0-20200218184317-f459e2d13664
github.com/sensu/sensu-go/api/core/v2 v2.14.0
github.com/shirou/gopsutil/v3 v3.22.4
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2066,6 +2066,8 @@ github.com/riemann/riemann-go-client v0.5.1-0.20211206220514-f58f10cdce16 h1:bGX
github.com/riemann/riemann-go-client v0.5.1-0.20211206220514-f58f10cdce16/go.mod h1:4rS0vfmzOMwfFPhi6Zve4k/59TsBepqd6WESNULE0ho=
github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robbiet480/go.nut v0.0.0-20220219091450-bd8f121e1fa1 h1:YmFqprZILGlF/X3tvMA4Rwn3ySxyE3hGUajBHkkaZbM=
github.com/robbiet480/go.nut v0.0.0-20220219091450-bd8f121e1fa1/go.mod h1:pL1huxuIlWub46MsMVJg4p7OXkzbPp/APxh9IH0eJjQ=
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff h1:+6NUiITWwE5q1KO6SAfUX918c+Tab0+tGAM/mtdlUyA=
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
Expand Down
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/twemproxy"
_ "github.com/influxdata/telegraf/plugins/inputs/udp_listener"
_ "github.com/influxdata/telegraf/plugins/inputs/unbound"
_ "github.com/influxdata/telegraf/plugins/inputs/upsd"
_ "github.com/influxdata/telegraf/plugins/inputs/uwsgi"
_ "github.com/influxdata/telegraf/plugins/inputs/varnish"
_ "github.com/influxdata/telegraf/plugins/inputs/vault"
Expand Down
61 changes: 61 additions & 0 deletions plugins/inputs/upsd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# UPSD Input Plugin

This plugin reads data of one or more Uninterruptible Power Supplies
from an upsd daemon using its NUT network protocol.

## Requirements

upsd should be installed and it's daemon should be running.

## Configuration

```toml
[[inputs.upsd]]
## A running NUT server to connect to.
# If not provided will default to 127.0.0.1
# server = "127.0.0.1"

## The default NUT port 3493 can be overridden with:
# port = 3493

# username = "user"
# password = "password"
```

## Metrics

This implementation tries to maintain compatibility with the apcupsd metrics:

- upsd
- tags:
- serial
- ups_name
- model
- fields:
- status_flags ([status-bits][])
- input_voltage
- load_percent
- battery_charge_percent
- time_left_ns
- output_voltage
- internal_temp
- battery_voltage
- input_frequency
- battery_date
- nominal_input_voltage
- nominal_battery_voltage
- nominal_power
- firmware

With the exception of:

- tags:
- status (string representing the set status_flags)
- fields:
- time_on_battery_ns

## Example Output

```shell
upsd,serial=AS1231515,ups_name=name1 load_percent=9.7,time_left_ns=9800000,output_voltage=230.4,internal_temp=32.4,battery_voltage=27.4,input_frequency=50.2,input_voltage=230.4,battery_charge_percent=100,status_flags=8i 1490035922000000000
```
188 changes: 188 additions & 0 deletions plugins/inputs/upsd/upsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package upsd

import (
"fmt"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal/choice"
"github.com/influxdata/telegraf/plugins/inputs"
nut "github.com/robbiet480/go.nut"
"strings"
)

//See: https://networkupstools.org/docs/developer-guide.chunked/index.html

const defaultAddress = "127.0.0.1"
const defaultPort = 3493

type Upsd struct {
Server string
Port int
Username string
Password string
Log telegraf.Logger `toml:"-"`

batteryRuntimeTypeWarningIssued bool
}

func (*Upsd) Description() string {
return "Monitor UPSes connected via Network UPS Tools"
}

var sampleConfig = `
## A running NUT server to connect to.
# server = "127.0.0.1"
# port = 3493
# username = "user"
# password = "password"
`

func (*Upsd) SampleConfig() string {
return sampleConfig
}

func (u *Upsd) Gather(acc telegraf.Accumulator) error {
upsList, err := u.fetchVariables(u.Server, u.Port)
if err != nil {
return err
}
for name, variables := range upsList {
u.gatherUps(acc, name, variables)
}
return nil
}

func (u *Upsd) gatherUps(acc telegraf.Accumulator, name string, variables []nut.Variable) {
metrics := make(map[string]interface{})
for _, variable := range variables {
name := variable.Name
value := variable.Value
metrics[name] = value
}

tags := map[string]string{
"serial": fmt.Sprintf("%v", metrics["device.serial"]),
"ups_name": name,
//"variables": variables.Status not sure if it's a good idea to provide this
"model": fmt.Sprintf("%v", metrics["device.model"]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What type does metrics["device.model"] usually have?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected to be string, e.g. "Model 12345" and just to be sure fmt.Sprintg

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't use fmt.Sprintf if not necessary. This way we will notice if the protocol changes (which should never happen actually).

}

// For compatibility with the apcupsd plugin's output we map the status string status into a bit-format
status := u.mapStatus(metrics, tags)

timeLeftS, ok := metrics["battery.runtime"].(int64)
if !ok && !u.batteryRuntimeTypeWarningIssued {
u.Log.Warnf("'battery.runtime' type is not int64")
u.batteryRuntimeTypeWarningIssued = true
}

fields := map[string]interface{}{
"status_flags": status,
"ups.status": metrics["ups.status"],
srebhan marked this conversation as resolved.
Show resolved Hide resolved
"input_voltage": metrics["input.voltage"],
"load_percent": metrics["ups.load"],
"battery_charge_percent": metrics["battery.charge"],
"time_left_ns": timeLeftS * 1_000_000_000, //Compatibility with apcupsd metrics format
"output_voltage": metrics["output.voltage"],
"internal_temp": metrics["ups.temperature"],
"battery_voltage": metrics["battery.voltage"],
"input_frequency": metrics["input.frequency"],
"nominal_input_voltage": metrics["input.voltage.nominal"],
"nominal_battery_voltage": metrics["battery.voltage.nominal"],
"nominal_power": metrics["ups.realpower.nominal"],
"firmware": metrics["ups.firmware"],
"battery_date": metrics["battery.mfr.date"],
}

acc.AddFields("upsd", fields, tags)
}

func (u *Upsd) mapStatus(metrics map[string]interface{}, tags map[string]string) uint64 {
status := uint64(0)
statusString := fmt.Sprintf("%v", metrics["ups.status"])
statuses := strings.Fields(statusString)
//Source: 1.3.2 at http://rogerprice.org/NUT/ConfigExamples.A5.pdf
//apcupsd bits:
//0 Runtime calibration occurring (Not reported by Smart UPS v/s and BackUPS Pro)
//1 SmartTrim (Not reported by 1st and 2nd generation SmartUPS models)
//2 SmartBoost
//3 On line (this is the normal condition)
//4 On battery
//5 Overloaded output
//6 Battery low
//7 Replace battery
if choice.Contains("CAL", statuses) {
status |= 1 << 0
tags["status_CAL"] = "true"
}
if choice.Contains("TRIM", statuses) {
status |= 1 << 1
tags["status_TRIM"] = "true"
}
if choice.Contains("BOOST", statuses) {
status |= 1 << 2
tags["status_BOOST"] = "true"
}
if choice.Contains("OL", statuses) {
status |= 1 << 3
tags["status_OL"] = "true"
}
if choice.Contains("OB", statuses) {
status |= 1 << 4
tags["status_OB"] = "true"
}
if choice.Contains("OVER", statuses) {
status |= 1 << 5
tags["status_OVER"] = "true"
}
if choice.Contains("LB", statuses) {
status |= 1 << 6
tags["status_LB"] = "true"
}
if choice.Contains("RB", statuses) {
status |= 1 << 7
tags["status_RB"] = "true"
}
return status
}

func (u *Upsd) fetchVariables(server string, port int) (map[string][]nut.Variable, error) {
client, err := nut.Connect(server, port)
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}

if u.Username != "" && u.Password != "" {
_, err = client.Authenticate(u.Username, u.Password)
if err != nil {
return nil, fmt.Errorf("auth: %w", err)
}
}

upsList, err := client.GetUPSList()
if err != nil {
return nil, fmt.Errorf("getupslist: %w", err)
}

defer func() {
_, disconnectErr := client.Disconnect()
if disconnectErr != nil {
err = fmt.Errorf("disconnect: %w", disconnectErr)
}
}()

result := make(map[string][]nut.Variable)
for _, ups := range upsList {
result[ups.Name] = ups.Variables
}

return result, err
}

func init() {
inputs.Add("upsd", func() telegraf.Input {
return &Upsd{
Server: defaultAddress,
Port: defaultPort,
}
})
}
Loading