Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ GitCommit := $(shell git rev-parse HEAD)
LDFLAGS := "-s -w -X github.com/tschaefer/rpinfo/version.Version=$(Version) -X github.com/tschaefer/rpinfo/version.GitCommit=$(GitCommit)"

.PHONY: all
all: fmt lint dist
all: fmt lint test dist

.PHONY: fmt
fmt:
Expand All @@ -29,6 +29,10 @@ checksum:
done && \
cd ..

.PHONY: test
test:
test -z $(shell go test ./... 2>&1 >/dev/null || echo 1) || (echo "[WARN] Fix test issues" && exit 1)

.PHONY: clean
clean:
rm -rf bin
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,17 @@ Start the server on `localhost:8080` by default.
```
For further configuration, see the command-line options below.

| Flag | Description | Default |
|-------------------|--------------------------------------|-------------|
| `-H`, `--host` | Host to bind the server to | `localhost` |
| `-p`, `--port` | Port to run the server on | `8080` |
| `-a`, `--auth` | Enable bearer token authentication | `false` |
| `-t`, `--token` | Bearer token used for authentication | |
| `-m`, `--metrics` | Enable Prometheus metrics endpoint | `false` |
| `-r`, `--redoc` | Enable ReDoc API documentation | `false` |
| `-h`, `--help` | Show help for the server command | |
| Flag | Description | Default |
|----------------------|-------------------------------------------------|-------------|
| `-H`, `--host` | Host to bind the server to | `localhost` |
| `-p`, `--port` | Port to run the server on | `8080` |
| `-a`, `--auth` | Enable bearer token authentication | `false` |
| `-t`, `--token` | Bearer token used for authentication | |
| `-m`, `--metrics` | Enable Prometheus metrics endpoint | `false` |
| `-r`, `--redoc` | Enable ReDoc API documentation | `false` |
| `-f`, `--log-format` | Set log format: `text`, `structured`, `json` | `text` |
| `-l`, `--log-level` | Set log level: `debug`, `info`, `warn`, `error` | `info` |
| `-h`, `--help` | Show help for the server command | |

Additional a systemd service file and environment file are provided in the
[contrib directory](https://github.com/tschaefer/rpinfo/tree/main/contrib) for automatic startup on boot and management of the
Expand Down
4 changes: 4 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func init() {
serverCmd.Flags().StringP("token", "t", "", "Bearer Token for authentication")
serverCmd.Flags().BoolP("metrics", "m", false, "Enable Prometheus metrics")
serverCmd.Flags().BoolP("redoc", "r", false, "Enable ReDoc API documentation")
serverCmd.Flags().StringP("log-format", "f", "text", "Log format (text, structured, json)")
serverCmd.Flags().StringP("log-level", "l", "info", "Log level (debug, info, warn, error)")

rootCmd.AddCommand(serverCmd)
}
Expand All @@ -36,6 +38,8 @@ func RunServerCmd(cmd *cobra.Command, args []string) error {
config.Token, _ = cmd.Flags().GetString("token")
config.Metrics, _ = cmd.Flags().GetBool("metrics")
config.Redoc, _ = cmd.Flags().GetBool("redoc")
config.LogFormat, _ = cmd.Flags().GetString("log-format")
config.LogLevel, _ = cmd.Flags().GetString("log-level")

server.Run(config)

Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ module github.com/tschaefer/rpinfo
go 1.24.2

require (
github.com/VictoriaMetrics/metrics v1.38.0
github.com/gorilla/mux v1.8.1
github.com/spf13/cobra v1.9.1
github.com/VictoriaMetrics/metrics v1.38.0
github.com/stretchr/testify v1.11.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
golang.org/x/sys v0.15.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
github.com/VictoriaMetrics/metrics v1.38.0 h1:1d0dRgVH8Nnu8dKMfisKefPC3q7gqf3/odyO0quAvyA=
github.com/VictoriaMetrics/metrics v1.38.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4 changes: 4 additions & 0 deletions server/handler/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package handler

import (
"encoding/json"
"log/slog"
"net/http"

"github.com/tschaefer/rpinfo/server/log"
"github.com/tschaefer/rpinfo/version"
)

Expand All @@ -20,9 +22,11 @@ func JSONError(w http.ResponseWriter, status int, message string) {
}

func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
go log.Request(r, http.StatusInternalServerError, slog.LevelError, "not found")
JSONError(w, http.StatusNotFound, "not found")
}

func MethodNotAllowedHandler(w http.ResponseWriter, r *http.Request) {
go log.Request(r, http.StatusMethodNotAllowed, slog.LevelWarn, "method not allowed")
JSONError(w, http.StatusMethodNotAllowed, "method not allowed")
}
24 changes: 16 additions & 8 deletions server/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@ import (
"net/http"
"strings"

"github.com/tschaefer/rpinfo/server/log"
"github.com/tschaefer/rpinfo/vcgencmd"
)

type Handle struct {
Cmd vcgencmd.Exec
}

func runCmd(h Handle, w http.ResponseWriter, args ...string) map[string]string {
out := h.Cmd.Run(args...)
if out == nil {
func runCmd(h Handle, w http.ResponseWriter, r *http.Request, args ...string) map[string]string {
out, err := h.Cmd.Run(args...)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)

go log.RequestError(r, http.StatusInternalServerError, err.Error())
json.NewEncoder(w).Encode(map[string]string{"detail": "internal server error"})
return nil
}
Expand All @@ -31,46 +34,49 @@ func runCmd(h Handle, w http.ResponseWriter, args ...string) map[string]string {
}

func (h Handle) Temperature(w http.ResponseWriter, r *http.Request) {
temp := runCmd(h, w, "measure_temp")
temp := runCmd(h, w, r, "measure_temp")
if temp == nil {
return
}

go log.RequestInfo(r, http.StatusOK, "Fetched temperature")
json.NewEncoder(w).Encode(temp)
}

func (h Handle) Configuration(w http.ResponseWriter, r *http.Request) {
options := []string{"int", "str"}
config := make(map[string]string)
for _, opt := range options {
out := runCmd(h, w, "get_config", opt)
out := runCmd(h, w, r, "get_config", opt)
if out == nil {
return
}

maps.Copy(config, out)
}

go log.RequestInfo(r, http.StatusOK, "Fetched configuration")
json.NewEncoder(w).Encode(config)
}

func (h Handle) Voltages(w http.ResponseWriter, r *http.Request) {
options := []string{"core", "sdram_c", "sdram_i", "sdram_p"}
voltages := make(map[string]string)
for _, opt := range options {
out := runCmd(h, w, "measure_volts", opt)
out := runCmd(h, w, r, "measure_volts", opt)
if out == nil {
return
}

voltages[opt] = out["volt"]
}

go log.RequestInfo(r, http.StatusOK, "Fetched voltages")
json.NewEncoder(w).Encode(voltages)
}

func (h Handle) Throttled(w http.ResponseWriter, r *http.Request) {
throttled := runCmd(h, w, "get_throttled")
throttled := runCmd(h, w, r, "get_throttled")
if throttled == nil {
return
}
Expand All @@ -84,6 +90,7 @@ func (h Handle) Throttled(w http.ResponseWriter, r *http.Request) {
throttled["throttled"] = message
}

go log.RequestInfo(r, http.StatusOK, "Fetched throttled status")
json.NewEncoder(w).Encode(throttled)
}

Expand All @@ -95,7 +102,7 @@ func (h Handle) Clock(w http.ResponseWriter, r *http.Request) {
}
clock := make(map[string]string)
for _, opt := range options {
out := runCmd(h, w, "measure_clock", opt)
out := runCmd(h, w, r, "measure_clock", opt)
if out == nil {
return
}
Expand All @@ -111,5 +118,6 @@ func (h Handle) Clock(w http.ResponseWriter, r *http.Request) {
clock[opt] = value()
}

go log.RequestInfo(r, http.StatusOK, "Fetched clock rates")
json.NewEncoder(w).Encode(clock)
}
31 changes: 16 additions & 15 deletions server/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Licensed under the MIT license, see LICENSE in the project root for details.
package handler

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
Expand All @@ -15,45 +16,45 @@ import (

type mockRunnerSuccess struct{}

func (m mockRunnerSuccess) Run(args ...string) map[string]string {
func (m mockRunnerSuccess) Run(args ...string) (map[string]string, error) {
switch args[0] {
case "measure_temp":
return map[string]string{"temp": "45.0'C"}
return map[string]string{"temp": "45.0'C"}, nil
case "measure_volts":
switch args[1] {
case "core":
return map[string]string{"volt": "1.3500V"}
return map[string]string{"volt": "1.3500V"}, nil
case "sdram_c":
return map[string]string{"volt": "1.2000V"}
return map[string]string{"volt": "1.2000V"}, nil
case "sdram_i":
return map[string]string{"volt": "1.2000V"}
return map[string]string{"volt": "1.2000V"}, nil
case "sdram_p":
return map[string]string{"volt": "1.2250V"}
return map[string]string{"volt": "1.2250V"}, nil
default:
return nil
return nil, nil
}
case "get_config":
return map[string]string{"init_uart_clock": "0x2dc6c00", "overlay_prefix": "overlays/", "total_mem": "512"}
return map[string]string{"init_uart_clock": "0x2dc6c00", "overlay_prefix": "overlays/", "total_mem": "512"}, nil
case "get_throttled":
return map[string]string{"throttled": "0x50000"}
return map[string]string{"throttled": "0x50000"}, nil
case "measure_clock":
switch args[1] {
case "arm":
return map[string]string{"freq": "600000000"}
return map[string]string{"freq": "600000000"}, nil
case "core":
return map[string]string{"freq": "250000000"}
return map[string]string{"freq": "250000000"}, nil
default:
return map[string]string{"freq": "0"}
return map[string]string{"freq": "0"}, nil
}
default:
return nil
return nil, nil
}
}

type mockRunnerError struct{}

func (m mockRunnerError) Run(args ...string) map[string]string {
return nil
func (m mockRunnerError) Run(args ...string) (map[string]string, error) {
return nil, fmt.Errorf("command failed")
}

func Test_TemperatureReturnsJSON(t *testing.T) {
Expand Down
11 changes: 10 additions & 1 deletion server/handler/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strconv"

"github.com/VictoriaMetrics/metrics"
"github.com/tschaefer/rpinfo/server/log"
"github.com/tschaefer/rpinfo/vcgencmd"
"github.com/tschaefer/rpinfo/version"
)
Expand Down Expand Up @@ -48,9 +49,12 @@ func Metrics(w http.ResponseWriter, r *http.Request) {
var buffer bytes.Buffer
rpi.WritePrometheus(&buffer)
if _, err := w.Write(buffer.Bytes()); err != nil {

go log.RequestError(r, http.StatusInternalServerError, fmt.Sprintf("Failed to write metrics: %v", err))
http.Error(w, "Failed to write metrics", http.StatusInternalServerError)
return
}
go log.RequestInfo(r, http.StatusOK, "Served metrics")
}

func clock(kind string) float64 {
Expand Down Expand Up @@ -102,5 +106,10 @@ func voltage(kind string) float64 {

func exec(args ...string) map[string]string {
h := Handle{Cmd: vcgencmd.Cmd{}}
return h.Cmd.Run(args...)
out, err := h.Cmd.Run(args...)
if err != nil {
return nil
}

return out
}
Loading