commit 6ab6ce12c6d5cd60f205c9f5d9d33391b52c5f45 from: Oliver Lowe date: Wed Jan 04 02:30:30 2023 UTC dishy, cmd/dishy: implement reading and printing of device statistics commit - 1636c523802a3144d79ae3175abb6a3ea5d64778 commit + 6ab6ce12c6d5cd60f205c9f5d9d33391b52c5f45 blob - 147b0817f915ff2871fb23ca627da9371eb95400 blob + b2879e418897334a3a65bcc858fe147d66c8f3d0 --- cmd/dishy/dishy.go +++ cmd/dishy/dishy.go @@ -2,7 +2,9 @@ package main import ( "flag" + "fmt" "log" + "os" "olowe.co/dishy" ) @@ -11,6 +13,23 @@ const usage = "usage: dishy [-a address] command" var aFlag = flag.String("a", dishy.DefaultDishyAddr, "dishy device IP address") +func printStatus(client *dishy.Client) error { + stat, err := client.Status() + if err != nil { + return fmt.Errorf("read status: %w", err) + } + fmt.Fprintln(os.Stderr, stat) + fmt.Println("alerts:", stat.Alerts) + fmt.Println("id:", stat.DeviceInfo.Id) + fmt.Println("ready:", stat.ReadyStates) + fmt.Println("outage:", stat.Outage) + fmt.Println("gps:", stat.GpsStats) + fmt.Println("stowed:", stat.StowRequested) + fmt.Println("updates:", stat.SoftwareUpdateState) + fmt.Println("lowsnr:", stat.IsSnrPersistentlyLow) + return nil +} + func main() { log.SetFlags(0) log.SetPrefix("dishy:") @@ -36,6 +55,14 @@ func main() { err = client.Stow() case "unstow": err = client.Unstow() + case "stat": + err = printStatus(client) + case "metrics": + stat, err := client.Status() + if err != nil { + log.Fatal("read status: %v", err) + } + err = dishy.WriteOpenMetrics(os.Stdout, stat) } if err != nil { log.Fatalf("%s: %v", cmd, err) blob - 9534fb0d1ac62309fd51d135b4637ea51f9eaaac blob + 3d3730d6779ecd999f4a4d97942a0918dc669622 --- cmd/dishy/doc.go +++ cmd/dishy/doc.go @@ -11,6 +11,10 @@ The following commands are understood: Reposition dish in a vertical orientation for easier transport. unstow Reposition the dish in the orientation prior to unstowing. + stat + Print short device status and diagnostics. + metrics + Print device statistics in OpenMetrics/Prometheus format. The flag -a specifies the address to connect to dishy. By default this is the default IPv4 address and port that dishy listens on 192.168.100.1:9200. blob - 6893fbe340039216aea4a53d12a7662d90cc73a3 blob + 5b2c4d7e3d7799570fd20e7ce6d8c22f2c3dea28 --- dishy.go +++ dishy.go @@ -2,6 +2,7 @@ package dishy import ( "context" + "fmt" "time" "google.golang.org/grpc" @@ -69,6 +70,71 @@ func (c *Client) Reboot() error { return err } +func (c *Client) Status() (*device.DishGetStatusResponse, error) { + req := &device.Request{ + Request: &device.Request_GetStatus{ + GetStatus: &device.GetStatusRequest{}, + }, + } + resp, err := c.do(req) + return resp.GetDishGetStatus(), err +} + +func (c *Client) Interfaces() ([]device.NetworkInterface, error) { + req := &device.Request{ + Request: &device.Request_GetNetworkInterfaces{ + GetNetworkInterfaces: &device.GetNetworkInterfacesRequest{}, + }, + } + resp, err := c.do(req) + if err != nil { + return nil, err + } + if resp.GetGetNetworkInterfaces() == nil { + return nil, fmt.Errorf("no interfaces in response") + } + var ifaces []device.NetworkInterface + for _, iface := range resp.GetGetNetworkInterfaces().NetworkInterfaces { + if iface == nil { + continue + } + ifaces = append(ifaces, *iface) + } + return ifaces, nil +} + +func (c *Client) TransceiverTelemetry() (*device.TransceiverGetTelemetryResponse, error) { + req := &device.Request{ + Request: &device.Request_TransceiverGetTelemetry{ + TransceiverGetTelemetry: &device.TransceiverGetTelemetryRequest{}, + }, + } + resp, err := c.do(req) + if err != nil { + return nil, fmt.Errorf("do request: %w", err) + } + if resp.GetTransceiverGetTelemetry() == nil { + return nil, fmt.Errorf("no telemetry in response") + } + return resp.GetTransceiverGetTelemetry(), nil +} + +func (c *Client) TransceiverStat() (*device.TransceiverGetStatusResponse, error) { + req := &device.Request{ + Request: &device.Request_TransceiverGetStatus{ + TransceiverGetStatus: &device.TransceiverGetStatusRequest{}, + }, + } + resp, err := c.do(req) + if err != nil { + return nil, fmt.Errorf("do request: %w", err) + } + if resp.GetTransceiverGetStatus() == nil { + return nil, fmt.Errorf("no telemetry in response") + } + return resp.GetTransceiverGetStatus(), nil +} + func (c *Client) do(req *device.Request) (*device.Response, error) { ctx := context.Background() if c.Timeout > 0 { blob - /dev/null blob + 48e0fd5734d22c73f4a5744292fce191962c3fef (mode 644) --- /dev/null +++ metrics.go @@ -0,0 +1,44 @@ +package dishy + +import ( + "fmt" + "io" + "text/template" + + "olowe.co/dishy/device" +) + +const metricsPage string = ` +# HELP dishy_uptime_seconds Seconds since last boot. +# TYPE dishy_uptime_seconds counter +dishy_uptime_seconds {{ .DeviceState.UptimeS }} +# HELP dishy_pop_ping_drop_rate +# TYPE dishy_pop_ping_drop_rate gauge +dishy_pop_ping_drop_rate {{ .PopPingDropRate }} +# HELP dishy_pop_ping_latency_milliseconds +# TYPE dishy_pop_ping_latency gauge +dishy_pop_ping_latency_milliseconds {{ .PopPingLatencyMs }} +# HELP dishy_downlink_throughput Received bytes per second. +# TYPE dishy_downlink_throughput gauge +dishy_downlink_throughput {{ .DownlinkThroughputBps }} +# HELP dishy_uplink_throughput Transmitted bytes per second. +# TYPE dishy_uplink_throughput gauge +dishy_uplink_throughput {{ .UplinkThroughputBps }} +# HELP dishy_obstruction_percentage +# TYPE dishy_obstruction_percentage gauge +dishy_obstruction_percentage {{ .ObstructionStats.FractionObstructed }} +` + +var metricsTmpl = template.Must(template.New("metrics").Parse(metricsPage)) + +// WriteOpenMetrics writes any metrics found in status in [OpenMetrics] +// format to w for use in systems such as Prometheus and VictoriaMetrics. +// +// [OpenMetrics]: https://openmetrics.io/ +func WriteOpenMetrics(w io.Writer, status *device.DishGetStatusResponse) error { + err := metricsTmpl.Execute(w, status) + if err != nil { + return fmt.Errorf("execute template: %w", err) + } + return nil +}